Back to Research
Architecture2025-11-25·7 min read read

Migrating to a Modular Monolith: Patterns That Actually Work

modular monolithmigrationarchitecturerefactoring
Migrating to a Modular Monolith: Patterns That Actually Work

The worst codebase we ever inherited was a Node.js application for a property management company. 180,000 lines of TypeScript in a single src directory. No folders for domains, no service layer, no separation between HTTP handling and business logic. Route handlers directly queried the database, applied business rules, sent emails, and returned HTML. We restructured it into seven well-defined modules over four months without a single day of downtime.

The first pattern is Strangler Fig for modules. You do not refactor the entire codebase at once. You identify the next feature that needs building and build it in a new module with clean architecture. Old code stays in place. New code lives in the new structure. Over time, as features and fixes accumulate, more code moves into modules and less remains in the legacy tangle.

The second pattern is the Anti-Corruption Layer. When new module code needs to interact with legacy code, create an interface describing what you need and implement it as a thin wrapper around the legacy functions. This prevents legacy patterns from leaking into clean modules. When you eventually refactor the legacy code, the module never changes.

The third pattern is an in-process Event Bus. Modules communicate through typed events, not direct function calls. Payments publishes PaymentReceived. Notifications subscribes and sends emails. Accounting subscribes and updates the ledger. A simple typed EventEmitter is sufficient. No message broker required.

The fourth pattern is Database Schema as Module Boundary. Each module owns its database tables in a named PostgreSQL schema. Other modules access data only through the owning module's public API, enforced by import path lint rules.

The fifth pattern is Incremental Extraction. Prioritize modules by three criteria: upcoming feature work (immediate payoff), isolation from other domains (easiest to extract), and bug density (immediate quality improvement). Extract the highest-scoring domain area next.

The key metric we tracked was new code percentage. What percentage of weekly code went into clean modules versus legacy? Month 1: 40% new. Month 4: 90% new. The codebase improved every sprint without dedicated refactoring time.

The mistake we see most: attempting to extract all modules at once. Teams plan a big bang migration, spend two months refactoring without shipping features, lose business patience, and end up with a half-finished mess. Incremental extraction avoids this entirely because features keep shipping throughout.

About the Author

Fordel Studios

AI-native app development for startups and growing teams. 14+ years of experience shipping production software.

Want to discuss this further?

We love talking shop. If this article resonated, let's connect.

Start a Conversation

Ready to build
something real?

Tell us about your project. We'll give you honest feedback on scope, timeline, and whether we're the right fit.

Start a Conversation