
In 2026, the “Microservices First” era has officially cooled.
Engineering teams have realized that for many use cases, distributed systems introduce more problems—latency, complex networking, and data inconsistency—than they solve.
The industry is shifting toward the Modular Monolith.
This isn’t just a “big ball of mud” renamed.
It’s a sophisticated architectural pattern where business logic is strictly isolated into independent modules, but the entire system runs in a single process.
For developers, this means simpler deployment and a streamlined infrastructure.
If you’re managing a database behind a private IP, the Modular Monolith allows you to secure that single connection point without the nightmare of managing VPC peering or Service Meshes for dozens of tiny apps.
🔬 The Deep Dive: Solving the “Shared Database” Dilemma
The most significant risk in a monolith is “Database Entanglement.” If every module can query every table, your code becomes a spiderweb. To build a true Modular Monolith in NestJS, you must enforce boundaries at the persistence layer.
1. Hard Boundaries: The “One Module, One Repository” Rule
Even though your modules share a single database connection (and likely a single schema), they should behave as if they don’t.
-
The Rule:
OrdersModulemust never inject theUserRepository. -
The Solution: If
Ordersneeds user data, it should request it via an internalQueryBusor an Event. This preserves the “Microservice Readiness” of the module.
2. Handling Transactions across Modules
In a distributed microservice, you’d need a Saga pattern. In a Monolith, we have the luxury of ACID transactions. However, we must pass the “Transaction Context” without leaking implementation details.
// orders.service.ts - Advanced Transaction Management
@Injectable()
export class OrdersService {
constructor(
private dataSource: DataSource, // Using TypeORM
private eventEmitter: EventEmitter2
) {}
async checkout(userId: string, cartItems: any[]) {
return await this.dataSource.transaction(async (manager) => {
// 1. Create the Order
const order = await manager.save(Order, { userId, total: 100 });
// 2. Emit a 'Critical' event and pass the manager
// This allows the InventoryModule to decrement stock within the SAME transaction
await this.eventEmitter.emitAsync('order.created.tx', {
order,
manager
});
return order;
});
}
}
3. Avoiding Circular Dependencies
As a monolith grows, Users will eventually need Orders, and Orders will need Users. NestJS will throw a Circular dependency error.
How to fix it for real:
-
Shared Kernel: Move shared interfaces, DTOs, and Constants to a
commondirectory that has zero dependencies on your feature modules. -
Event-Driven Decoupling: Instead of
Module AcallingModule B, haveModule Aemit an event thatModule Blistens to. This breaks the link entirely.
💻 Code Example: The Decoupled Listener
Here is how the “other side” of that transaction looks.
Notice how the InventoryModule uses the passed EntityManager to stay within the same database transaction.
// inventory.listener.ts
@Injectable()
export class InventoryListener {
@OnEvent('order.created.tx')
async handleOrderCreated(payload: { order: any, manager: EntityManager }) {
const { order, manager } = payload;
// We use the 'manager' provided by the caller to ensure
// that if stock update fails, the Order creation also rolls back.
await manager.update(Product, order.productId, {
stock: () => "stock - 1"
});
}
}
🔒 Infrastructure: The Private IP Advantage
This architecture shines when it comes to security. By utilizing a Private IP for your database, you create a fortress.
-
Minimal Surface Area: Your database has no public endpoint. Only the NestJS process, living within your VPC, can talk to it.
-
Connection Efficiency: You manage a single connection pool. You don’t have to worry about 20 microservices each spinning up 10 connections and hitting the
max_connectionslimit of your RDS or Postgres instance.
Scale Logic, Not Just Servers
Choosing a Modular Monolith isn’t about being “lazy”—it’s about being strategic.
It allows you to focus on your domain logic while keeping your infrastructure footprint small and secure.
When your “Orders” module eventually gets so much traffic that it needs its own dedicated CPU and memory, the migration is simple: move that folder to a new repo, swap the EventEmitter for a RabbitMQ or Kafka client, and you’ve officially moved to microservices.
Build for tomorrow, but ship today.
Useful links below:
Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service
Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2
Best email marketing automation solution on the market! http://www.aweber.com/?373860
Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29
Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff
Buy me a coffee https://buymeacoffee.com/tyronneratcliff



