Monolith First in 2026: The Case for Resisting Microservices Until You Have To
The microservices conversation has quietly inverted. After a decade in which splitting the system was the default answer to almost any architectural question, teams in 2026 are relearning something Martin Fowler wrote back in 2015: start with a monolith. The monolith first position was contrarian then. It is close to mainstream now, mostly because a lot of engineering organisations have paid the distributed-systems bill and did not like the invoice.
This is not an argument that microservices are wrong. It is an argument about sequencing. Service boundaries are expensive to move and nearly impossible to guess correctly before you have shipped the product several times. Choosing them early means committing to a set of assumptions at exactly the moment you know least about your domain.
What Monolith First Actually Claims
The claim is narrow and worth stating precisely, because it gets caricatured in both directions.
Monolith first does not say monoliths scale better, that microservices never pay off, or that a single codebase is inherently simpler. It says that the information required to draw good service boundaries only exists after the system has been in production and under change pressure for a while. A monolith lets you move a boundary with a refactor. A microservice architecture makes the same change a migration involving two deployments, an API contract, a data move, and a backwards-compatibility window.
So the argument is about the cost of being wrong. Early on, you are almost certainly wrong about where the seams belong, and the monolith is the architecture with the cheapest correction.
The Cost of Microservices Before Product-Market Fit
The overhead of a distributed system is easy to underestimate because most of it is not code. It is operational surface area that arrives all at once.
Every in-process call becomes a network call. A method invocation that took microseconds and either worked or threw becomes a request that can be slow, partially fail, time out, or succeed after the caller gave up. Each of those needs a decision: retry policy, idempotency, timeout budget, circuit breaker. That is real engineering work spent on plumbing your product does not sell.
Observability stops being optional. In a monolith, a stack trace tells the story. Across seven services, a single user action produces seven partial stories, and you cannot reconstruct it without distributed tracing, correlation IDs propagated through every hop, and log aggregation. You need this infrastructure on day one rather than when the team is large enough to justify it.
Schema changes require coordination. Adding a column in a monolith is one migration in one deploy. When two services read the same conceptual entity, the same change becomes a sequenced rollout: expand the schema, deploy the readers, backfill, deploy the writers, contract. Multiply that by the number of times an early-stage product changes its data model, which is constantly.
Local development gets heavier. New engineers need the whole system running to work on a feature that touches two services. Docker Compose files grow, onboarding slows, and the feedback loop stretches from seconds to minutes.
Ownership gets ambiguous before the org is big enough for it to be clear. Microservices pay off largely as an organisational tool: they let independent teams deploy without coordinating. With twelve engineers, you do not have independent teams. You have one team maintaining twelve deployment pipelines.
None of these costs are fatal. All of them are real, and all of them arrive before the benefits do. Our tech stack strategy work exists largely because this trade-off is easy to get wrong in the direction that feels most sophisticated.
The Signals That Genuinely Justify a Split
Splitting is right when the monolith is causing measurable friction, not when it feels inelegant. Three signals are reliable.
The first is deploy lockstep between unrelated teams. When the billing team cannot ship because the reporting team's change is in the same release candidate, and this happens weekly rather than occasionally, coordination cost has become structural. Note the qualifier: a slow release process is often a CI or test-suite problem, not an architecture problem. Fix the cheaper thing first.
The second is genuinely incompatible resource profiles. If one part of the system is a memory-hungry batch process and another is a latency-sensitive API, and they are competing for the same instances, you have a real reason to separate them. This is about the shape of the workload, not the shape of the code. A component that needs to scale to twenty instances while everything else needs two is a legitimate candidate for extraction.
The third is database contention from incompatible access patterns. Long analytical queries locking tables that transactional writes need is a real problem, though it is worth checking whether a read replica solves it first. Extracting a service to fix a query pattern is an expensive way to buy something a replica might give you in an afternoon.
What is not a signal: the codebase is large, a conference talk was persuasive, a new hire is used to microservices, or the deploy takes eleven minutes. Codebase size is addressed by modularity, which does not require the network.
The Modular Monolith: Boundaries Without the Network
Most of what people want from microservices is boundaries, not distribution. A modular monolith gives you the first without the second: one deployable unit containing modules with explicit, enforced interfaces.
In practice this means organising by business capability rather than technical layer. Instead of top-level Controller, Entity, and Repository directories, you have Billing, Identity, and Reporting, each containing its own controllers, entities, and services. Each module exposes a deliberately small public surface, typically a handful of application services and read-model DTOs, and keeps everything else internal. Cross-module communication goes through those public services or through domain events, never by reaching into another module's entities or repositories.
The discipline that makes this work is refusing to share entities across modules. If Billing needs customer data, it does not import the Identity entity. It calls an identity service that returns a DTO, or it keeps its own minimal customer projection updated by events. That single rule is what makes a later extraction feasible, because the module already talks to its neighbours the way a service would.
We wrote about the architecture itself in more depth in when to go modular monolith first. The point worth repeating here is that modularity is a property of the code and the team, not of the deployment topology.
Enforcing Boundaries in Symfony with PHPStan
Conventions that are not enforced decay. In a Symfony codebase, static analysis is the cheapest enforcement available, and it runs in CI rather than depending on reviewer vigilance.
The mechanism is straightforward. With modules under src/Billing, src/Identity, and src/Reporting, and each exposing a public API under src/<Module>/Application, you configure PHPStan with a rule that forbids any class in one module namespace from referencing a class in another module's non-public namespace. Community extensions for this exist, and a custom rule implementing PHPStan\Rules\Rule that inspects class references and compares namespace prefixes is perhaps thirty lines. Either way, the build fails when someone imports App\Identity\Domain\Customer from inside App\Billing.
Two details make the difference between a rule that helps and one the team disables. First, ship it with a baseline. An existing codebase will have hundreds of violations, and a rule that fails the build on day one gets switched off by lunchtime. Generate a baseline, forbid new violations, and burn down the existing ones as you touch the code. Second, keep an explicit allowlist for the legitimate exceptions, such as shared kernel value objects, and review it. When the allowlist starts growing, that is information about your boundaries, not a reason to widen the rule.
The result is a codebase where the boundary is real. It is checked mechanically on every pull request, and when a module genuinely needs to become a service, the extraction is mostly a matter of replacing a service call with an HTTP or message-bus call, because nothing else was ever coupled. This kind of enforcement is a standard part of what we look for in code quality consulting engagements, precisely because it prevents the drift that makes later decisions expensive.
Splitting Later Is Not a Failure Mode
The objection to monolith first is that extracting a service from a monolith is painful. It can be, but that pain is a function of how modular the monolith was, not of the split itself. Extracting a well-bounded module with no shared entities and event-based communication is a contained project. Extracting from a genuine big ball of mud is awful, which is an argument for modularity, not for premature distribution.
It is also worth being honest about the alternative. Rejoining microservices that were split along the wrong lines is harder than splitting a monolith along the right ones. The failure mode of over-splitting is a distributed monolith, where services must be deployed together and share a database, and you now pay both bills. If you are already there, the way back is usually incremental consolidation rather than a rewrite, which is the territory our legacy code optimization work covers.
How to Decide
Write down the specific friction you are trying to remove, then ask whether a service boundary is the cheapest way to remove it. Most of the time the honest answer is a module boundary, a read replica, a faster test suite, or clearer ownership inside one codebase. When the answer really is a service, you will be able to name the team that cannot deploy, the workload that cannot share instances, or the access pattern that cannot share a database. If you cannot name it, you are buying insurance against a problem you have not got, at a premium you are paying today.
Monolith first is not conservatism. It is keeping the option to be wrong cheaply, which at the start is the most valuable option you have.
If you are making this call for a new build, or trying to work out whether the microservices you already have are helping, we are happy to look at it with you. Write to hello@wolf-tech.io or visit wolf-tech.io. We also do this as focused architecture reviews within our custom software development engagements.

