typescript and javascript logo
author avatar

Grzegorz Dubiel

06-08-2026

Building a Modular Monolith for Future Scale

Often when building a greenfield product, especially in a startup environment, we need to move forward quickly and be prepared for pivots. That's why, for many teams, thinking about robust architecture and future scalability is often not the case, and sometimes it is not a problem if the business behind the project is not complex. But... what if the problem you solve is complex and spans multiple domains from day one, or you need to operate on large data volumes? In that case, good architecture plays a very important role. Without it, your team will quickly get lost and slow down, ending up debugging bugs caused by spaghetti code and rewriting parts of the application constantly on every pivot and every new major functionality, while struggling with a data model and skyrocketing technical debt from day one. On the other hand, in such an early-stage project, extracting microservices per domain would be costly and create operational overhead, so we need to have something in the middle. Fortunately, we have Modular Monolith Architecture.

What is Modular Monolith Architecture in a few words

If we want to summarize this in a few words, a Modular Monolith is a monolithic application comprised of loosely coupled modules/services that typically use a shared database while keeping their data models isolated and communicating with each other via APIs or events. You can think of it as microservices-like packed into one application that is a single deployable unit.

Node.js is a very flexible environment, great for implementing such an architecture, either by implementing it yourself or by using frameworks like Nest.js, which encourage a modular architecture and are a good fit for a Modular Monolith.

The additional advantage of the Modular Monolith is that when you make a mistake while extracting services, or you want to get rid of one of the existing ones, you often only need to adjust the project structure and module boundaries and move on.

Define Bounded Context first

If you made the decision to start your project with a Modular Monolith Architecture, do not jump directly into the code and start blindly creating modules. Bring stakeholders, domain experts, and team members together to define bounded contexts, which will help you structure the application by business domains.

Bounded Context is a crucial concept in DDD for clear strict domain boundaries and preventing overlapping logic between different parts of your system. This concept allows you to define independent domain models; for example, User can mean different things in the Identity(auth) context, where it holds information about application roles and connected identity providers, while in the Billing context it holds information about tax data and payment methods.

Data segregation

Data segregation is crucial when it comes to building a modular monolith, and if you've defined bounded contexts in your application, it should be much easier to separate models, but how do you technically do it?

Let's start from the beginning. If we think about a monolith, we think about an application that operates on a single data model. For example, we have a typical case of an e-commerce app where we have entities such as user, product, merchant, delivery, invoice; each of those entities has properties from every context. Let's zoom in on the user:

typescript

I've intentionally added comments to emphasize the different domains packed into this single user model; when you have more entities like this and the application grows, it is so hard to keep up with adding new features without destroying the logic, and the same issues apply to reading this code and testing.

In many microservice architectures, we would have a separate database for each service or domain, and in those databases our User entity would mean something else depending on the business context.

One of the benefits of the modular monolith is low operational overhead, and using a separate database per domain significantly reduces this benefit. But there are solutions for achieving such isolation within a single database.

The Pattern of Naming convention

The first approach is to enforce a naming convention for entities. For example, our god user is split into 3 tables/collections in the database, such as: identity_users, order_users, logistics_users. As you can see, the context comes first in the table name, and that way your data is segregated. The hard rule here is that there should be no joins between those tables, and each module should only operate on its own data model. Things like JOIN identy_users.id = order_users.id are strictly forbidden.

The cons are, of course, weak isolation guarded only by convention, not enforced by the database engine, and the fact that it will be harder to extract microservices. But this approach is very fast and simple and does not require any special database features, which, depending on the database used in a system, can be an advantage. For example, there is no schema in MongoDB in the same sense as in PostgreSQL, and to have data really separated you would need to create a database per domain, which adds complexity around selecting the right database per domain and managing access patterns in the application code.

The Pattern of Schema per Module

In a database like PostgreSQL, we can use a schema per module and have clear logical isolation per module without polluting table names with a special context prefix. When the time comes to extract a microservice, you can move the schema to a separate database as part of extracting the microservice. The rule of no joins still applies, despite cross-schema joins are possible in databases like PostgreSQL.

The main con is that it is not supported in all databases, and the very fact that this is a database-specific feature can be a problem.

Communication between modules

In a modular monolith, we can mentally map a module to a future microservice boundary, so let's say that I would like to implement an event-driven architecture — is it possible in a modular monolith? Yes, it is, and with proper implementation it should make a future move toward microservices easier. At the same time, in Node.js we do not necessarily need to use additional infrastructure such as message brokers; we can start with very simple things like an in-memory event mechanism: EventEmitter, and swap it later on if there is a need to extract a microservice.

We need to remember that our messaging mechanism should be hidden behind an interface and should be a separate package that allows us to swap the queue implementation, e.g. replace an in-memory mechanism with RabbitMQ.

We can also implement synchronous communication, but at this point we don't want to utilise any network calls. In order to achieve this goal, we need to operate on abstractions that will encapsulate the cross-module communication boundary, so for example module A, which calls module B, will have an adapter that will call module B's port. The port is like an interface that exposes the API of the functions that module B provides. When we look at our e-commerce app and assume that the Order module needs to check whether MFA is enabled for the user in the Identity module, we should:

  1. Prepare DTOs and interfaces as a contract:

typescript

  1. Implement the port in the Identity module:

typescript

  1. Implement the adapter in the Order module that will be used to call the facade of the Identity module:

typescript

  1. Use the adapter where needed in the Order module, for example:

typescript

The core business logic remains fully decoupled, and when the time comes and we need to extract modules into microservices, all we need to refactor is:

  1. In the Identity module, we introduce a controller that can call the facade and expose it via HTTP or gRPC:

typescript

  1. In the Order module, we need to swap the local adapter with a remote adapter, for example an HTTP adapter:

typescript

Using estabilished architecure patterns

The previous example of communication led us to this point, where we need to talk about how established patterns can help us build a future-proof modular monolith. To make our modules easy to convert into microservices, we need to have a way to completely isolate business logic from infrastructure, as we know that it should be the only concern in this case, because a modular monolith consists of logically structured modules within a single deployable application, whereas microservices are a system of independently deployable services - from this we know that at the stage of migration to microservices, infrastructure should be the only thing that changes.

Imagine we have a use case that manages orders and heavily modifies data in the database, and we need to move from the monolith to a separate microservice. In the best-case scenario, we would like to change only infrastructure-related code, with minimal effort and maximum safety for the business code, being confident that no critical bugs were introduced. When the team is confident, it will move faster when migrating to microservices.

Tests

I mentioned confidence in the previous paragraph - with tests, you would have another pillar of confidence when doing migration, but only if your tests are well structured and separated. In tests also, we should not mix business logic with infrastructure code, and we should ensure smoke tests that cover the enitre microservice components setup, so that after migration we can be confident that the new infrastructure works as expected and catch failures early.

When moving to microservices, the rules for tests are similar to those for production code. Tests for use cases and business logic should mostly pass unchanged; in most cases, the tests that need changes are the ones tied to infrastructure. If those two things are achieved after migration, you have a strong sign that the migration was successful.

Wrap up

These days, we talk a lot about building fast by throwing PRDs into prompts. But we talk less about scaling those applications. Remember: garbage in, garbage out. If you don't care about your app's architecture and you're thinking about a project that is built to scale, sooner or later poor architecture makes it much more likely that both AI tools and developers will struggle, producing more unusable slop, and your feature team will struggle to understand the business logic, as it will be blended with everything else in your codebase. Setting up architecture from day one costs very little compared with cleaning up architectural debt later; even if you don't decide to move to microservices in the future, you give yourself a much better chance of ending up with a technically reliable and scalable product that solves your business problem, or the business problem of the company you're working for, instead of introducing blockers and bugs.

typescript and javascript logogreg@aboutjs.dev

©Grzegorz Dubiel | 2026