Why Your Monolith Is Not the Problem
Microservices have been the dominant architectural recommendation in software engineering for a decade. The case for microservices, independent deployability, team autonomy, technology flexibility, is well-understood. Less well-understood is what actually happens when teams decompose a working monolith into microservices prematurely, and how many of the failures blamed on the monolith are actually failures of a different kind.
The pattern is consistent enough to be diagnostic: a team has a monolith that is getting hard to work with. They read about microservices, get excited, and begin decomposition. Two years later, they have a distributed monolith, all the coupling of the original plus the operational complexity of managing 30 services plus distributed transaction problems that did not exist before.
What Monolith Problems Actually Look Like
Before decomposing anything, it is worth being precise about which monolith problems you actually have. The common complaints are: slow build times, long test suites, deployment coupling (one bug blocks all deployments), and team coupling (two teams constantly stepping on each other's code).
Each of these has solutions that do not require service decomposition. Slow build times: incremental compilation, build caching (Turborepo, Gradle, Buck), and parallel test execution. Deployment coupling: feature flags and separate deployment tracks within the monolith. Team coupling: module-level ownership enforcement, documented interfaces between modules, and architecture fitness functions that prevent cross-module coupling from accreting silently.
The Modular Monolith Pattern
The modular monolith is the pattern that teams usually wish they had built before decomposing. It is a single deployable unit with strictly enforced module boundaries. Modules can only communicate through defined interfaces, not by direct function call or shared database table.
The enforcement mechanism varies by language. In Java or Kotlin, module visibility rules and JPMS can enforce this. In TypeScript, barrel exports and ESLint rules that block cross-module imports. In Python, explicit package structure and import linting.
A modular monolith solves the team coupling problem without introducing distributed systems complexity. It also creates the right foundation for future decomposition: when a module genuinely needs to be extracted into a service, the interface already exists and the extraction is straightforward.
The Strangler Fig: When Decomposition Makes Sense
The strangler fig pattern is the right decomposition approach for monoliths that genuinely need decomposition. The principle: extract functionality incrementally, routing requests to new services while the old monolith handles the rest. Never do a big-bang rewrite.
The practical version: put a proxy layer in front of your monolith. Route specific request paths to new services as they are built. The monolith handles everything the proxy does not explicitly route elsewhere. Over time, the monolith shrinks and the new services expand. You can stop at any point and still have a working system.
When Microservices Actually Make Sense
The cases where microservices are genuinely warranted: dramatically different scaling requirements, genuinely separate team structures operating with different release cadences, and components that need to be developed in different languages because the problem domain favors a different runtime.
Size is not a decomposition criterion. Coupling, scaling requirements, and team independence are decomposition criteria. A 500K line modular monolith with clean module boundaries and fast build times is a better engineering platform than 50 microservices with unclear ownership, cross-service transaction complexity, and a Kubernetes cluster that costs $3K per month to operate.