Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another comment regarding "1.1 Structure your solutions by business components" #1266

Open
mindactuate opened this issue Aug 17, 2023 · 1 comment

Comments

@mindactuate
Copy link

mindactuate commented Aug 17, 2023

Hi everyone! First, thank you so much for this resource that helps me not to forget important points while working on my platform that heavily relies on Node.js services.

I recognized that some of your best practices and especially your boilerplate code generator Practica "steer" people in the direction of microservices. I also thought of microservices while working on the initial system design of my platform, that will run on Google Cloud and Firebase. I decided against it because from my point of view it's too complex and also too expensive to start with. First, it is not feasible for me to run multiple DBs simultaneously. I cannot (or should not) scale them to zero and DBs are expensive to run 24/7. Second, while the latency caused by all the communication between the services via network isn't an issue for me (because I won't have thousands of reqs/second from the very beginning), the costs that come with exponential traffic might be an issue. I want to keep operational costs down. Furthermore it might be that I cannot scale services down to zero. It is more expensive to keep multiple services alive than just one. Third, the operational effort for deploying, running, monitoring and maintaining 10 instead of 1 or 2 services is immense for one or two people that need to handle thousands of other tasks, mostly not related to IT stuff but organization, acquisition, communication, support, etc. Sure, there are amazing tools and possibilities like CI/CD pipelines listening to small code changes and automatically deploying and starting new images; or infrastructure as code that can start the whole infrastructure at the press of a button. But learning these things takes hours, days, weeks, even years. As a guy who is starting up a project in his spare time, I cannot afford this. I need to keep things simple - at least at the beginning and until I can afford a team supporting me. (I don't want to go the common "startup" way that involves shiny marketing and shady contracts to get a few bucks so that I can hire someone who then needs to be coordinated and managed.)

But how can I "prepare" my system design, my structure and architecture so that it can grow naturally? What can I do now at the beginning so that I don't have to start over again if I want to enhance my system or if I want to replace a component with another? There are multiple reasons to go for microservices, or at least for a highly modular and decoupled design: As my startup grows, multiple people can work on different components without needing to understand the whole system or disturbing each other. When I identify some bottlenecks, a modular and loosely coupled system makes it easier to switch to another technology or programming language to optimize this specific component. Furthermore, microservices make it easy to scale components independently. So you can save lots of money because you don't have to multiply (= scale horizontally) the whole system.

But as I said: How can I manage the balancing act of keeping the system simple (easy to develop and to operate) and cheap on the one hand, but modular, expandable, and scalable on the other hand?

I would love to see such considerations and discussions in this repo. And I would like to get it started by sharing 2 (ugly) drafts I quickly prepared. The one picture shows a more decoupled microservices design with services communicating via network. All services have their own DB and clear interfaces so that other services can connect to them easily. The other picture shows a monolith with a component structure as you mentioned in best practice 1.1 Each component has its own schema and its own DB tables (in the same DB). The components cannot run independently (its a monolith or one docker container) but have clear interfaces so that other components can have "client functions" communicating with these interfaces.

I haven't had the time to write code examples but I will.

I really look forward to a valuable discussion. Thank you in advance!

Picture 1: Microservices decoupled
Microservices_decoupled

Picture 2: Easier design but with Microservices in mind
Easier_design_but_with_Microservices_in_mind

EDIT: I have to figure out if its possible to define a single DB with different Prisma schemas containing separate tables. Don't know if this is possible by design.

@mindactuate
Copy link
Author

mindactuate commented Aug 29, 2023

Hi together, just a short update. I currently try to use the following concept:

my-system
|─ services (components, modules)
|   |-- config-service
|   |    |-- entry-points
|   |    |    |-- validators # here I validate for example the request body (using zod) and convert to JS objects
|   |    |    |-- config-code-server.ts # provides a simple object with methods from "the inner world" of the service
|   |    |    |-- config-http-routes.ts # my http routes that are registered with my fastify HTTP server
|   |    |-- data-access
|   |    |    |-- validators # some of the columns in my Postgres are json type, I use validators (zod) to convert to JS objects
|   |    |    |-- config-db.ts # my DB calls using Prisma
|   |    |-- business-logic (domain)
|   |    |    |-- get-config-use-case.ts # here I do something with the config from DB and return it to the entry point
|   |-- another-service
|   |    |-- entry-points
|   |    |    |-- validators
|   |    |    |-- another-code-server.ts
|   |    |    |-- another-http-routes.ts
|   |    |-- data-access
|   |    |    |-- another-db.ts
|   |    |-- business-logic (domain)
|   |    |    |-- clients
|   |    |    |    |-- validators
|   |    |    |    |-- config-client.ts # imports config-code-server and provides functions to the "inner world" of another service
|   |    |    |    |-- google-maps-client.ts
|   |    |    |-- another-use-case.ts
|   |

Here an example for my config-code-server.ts:

import {
  getActiveConfig,
  getAllConfigs
} from '../business-logic/get-config-use-case';

export {
  getActiveConfig,
  getAllConfigs
};

And here my config-client.ts:

import { Config } from '@prisma/client';
import * as configCodeServer from '../../../config-service/entry-points/config-code-server';

export async function getActiveConfig(): Promise<Config> {
  const config = await configCodeServer.getActiveConfig();
  return config;
}

export async function getAllConfigs(): Promise<Config[]> {
  const configs = await configCodeServer.getAllConfigs();
  return configs;
}

I can also write my code server a little more abstract like this:

import { Config } from '@prisma/client';
import {
  getActiveConfig,
  getAllConfigs
} from '../business-logic/get-config-use-case';

export async function getActiveConfig(): Promise<Config> {
  const config = await getActiveConfig();
  return config;
}

export async function getAllConfigs(): Promise<Config[]> {
  const configs = await getAllConfigs();
  return configs;
}

config-code-server.ts and config-client.ts look quite similar leading to redundancy (don't repeat yourself?!). But all in all it would be the same or perhaps even worse with microservices.

I can use the client within another-service like this

import * as ConfigClient from '../config-client'

async function anotherFunc(){
  //...
  const config = await ConfigClient.getActiveConfig();
  //...
}

How do you find this approach? I saw other approaches with messages being published to an (in memory or external) event bus. You can find information to this here: kamilgrzybek.com/blog/posts/modular-monolith-integration-styles I tried to implement the "direct call" approach.

Here is also a great video on that topic. Modular Monolith - How to build one & lessons learned, from Milan Jovanovic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
@mindactuate and others