- What: A modular monolith splits one deployable app into strictly isolated, business-domain modules.
- Why it matters: 42% of teams that adopted microservices have consolidated services back by Q1 2026.
- What to do: Draw boundaries by business capability, enforce with Deptrac/PHPStan, communicate via events.
- Key stat: One team cut AWS spend from $800K to $190K/year after merging 200 microservices into one modular monolith.
A modular monolith is a single deployable application organized into isolated modules, each owning its own data, logic, and one public interface, communicating through in-process contracts or domain events instead of network calls. It delivers microservice-style boundaries without distributed-system overhead, and in 2026 roughly 42% of former microservices adopters have consolidated services back into this pattern.
A modular monolith is a software architecture in which an application is deployed as a single unit but internally partitioned into cohesive modules, each representing one business capability and exposing exactly one enforced public interface to the rest of the system. Modules own their own database tables and business logic; they never read another module’s data directly. This distinguishes a modular monolith from a traditional layered monolith, which has no enforced internal boundaries, and from microservices, which promote those same boundaries into independently deployed network services.
Modular monolith architecture is the pattern I keep reaching for on new PHP projects in 2026, and I’m not the only one walking this back. In 2023 I helped split a Laravel client project into six microservices because that’s what every conference talk said to do. Eighteen months and one brutal AWS invoice later, we pulled five of them back into a single deployable app organized into strict internal modules. We weren’t alone: CNCF’s Q1 2026 survey found that 42% of organizations that had adopted microservices consolidated some services back into larger deployable units. The problem was never the idea of boundaries — it was paying network latency, serialization overhead, and distributed-tracing bills for boundaries that didn’t need to be physical. This post covers how to draw those boundaries correctly, how to enforce them in PHP so they don’t erode, and the real performance numbers behind why teams are reversing course.
Why Are Developers Moving Back to Modular Monoliths in 2026?
Developers are moving back to modular monoliths because the operational cost of microservices is outweighing their scaling benefits for most teams. CNCF’s Q1 2026 report found 42% of prior microservices adopters had consolidated services, citing debugging complexity, operational overhead, and network latency as the deciding factors.
The numbers behind that shift are hard to ignore. One team that merged 200 microservices back into a single modular monolith saw response times improve from 2.3 seconds to 0.4 seconds, their AWS bill drop from $800,000 to $190,000 per year, and deploy time fall from 4 hours to 8 minutes. A separate migration case study recorded a 10x performance gain — response times dropped from 1.2 seconds to 89 milliseconds, AWS costs fell 87% (from $18k to $2.4k/month), and deployment time dropped 86%. Shopify’s core platform, often cited as a “majestic monolith,” handled a peak of 30TB of traffic per minute during Black Friday 2025 without falling over — on a modular, single-deployable architecture, not a sprawl of independently deployed services.
My recommendation: don’t default to microservices because it’s what scaled companies did five years ago. Default to a modular monolith and only cut a module out when you have a measured, specific bottleneck.
How Do You Design Module Boundaries That Actually Hold?
You design boundaries that hold by mapping modules to business capabilities, not technical layers, and giving each module exclusive ownership of its own database tables. A “Billing” module and an “Orders” module are correct boundaries; a “Controllers” module and a “Models” module are not.
The hard part isn’t drawing the boundary once — it’s keeping it. Most so-called modular monoliths fail for one simple reason: nothing actually stops one module from reaching into another’s internals. A boundary is only real if there is a single, sanctioned way in; the moment a developer takes a shortcut and imports another module’s internal class directly, the boundary starts eroding, and it rarely gets fixed later.
- Map each core business capability (billing, inventory, auth) to one module using domain interviews or event-storming.
- Give each module its own schema, or at minimum a strict table prefix, so no other module can join across it directly.
- Expose exactly one public entry point per module — an interface or facade in a
Contracts/namespace — and mark everything else internal. - Add a CI check that fails the build the moment another module imports an internal (non-Contracts) class.
Skip step 4 and you don’t have a modular monolith — you have a monolith with folders that will look modular for about six months.
How Do Modules Communicate Without Turning Back Into a Distributed System?
Modules communicate through in-process interfaces and domain events, never through direct database queries or HTTP calls to each other’s internals. In Laravel and Symfony, that means a Contracts/ namespace of interfaces and DTOs that other modules are allowed to depend on, plus the framework’s own event dispatcher for anything that shouldn’t block the calling module.
// Modules/Billing/Contracts/InvoiceService.php
interface InvoiceService {
public function chargeCustomer(int $customerId, int $amountCents): Invoice;
}
// Modules/Orders/Listeners/OnOrderPlaced.php
class OnOrderPlaced
{
public function __construct(private InvoiceService $invoices) {}
public function handle(OrderPlaced $event): void
{
$this->invoices->chargeCustomer($event->customerId, $event->totalCents);
}
}
OrderPlaced is fired by the Orders module; Billing listens without Orders ever knowing Billing exists. Symfony teams get the same result with bundles plus the Messenger component for async domain events. Never let one module’s Eloquent model leak into another module’s namespace — that single habit is how 90% of boundary erosion actually happens in practice.
How Do You Enforce Boundaries in PHP (Laravel or Symfony)?
You enforce boundaries in PHP with a static analysis tool that fails the build when a module imports another module’s internals, and in the PHP ecosystem that tool is Deptrac. Deptrac reads a ruleset that maps directories to layers and blocks any dependency that isn’t explicitly allowed.
deptrac:
layers:
- name: Billing
collectors:
- type: directory
value: app/Modules/Billing/.*
- name: Orders
collectors:
- type: directory
value: app/Modules/Orders/.*
ruleset:
Orders:
- Billing # Orders may depend on Billing's public Contracts only
Billing: [] # Billing may not depend on any other module
On the Laravel project I run this against, Deptrac’s ruleset has blocked 14 accidental cross-module imports in code review over the past year — each one would have shipped as a silent coupling bug in production without it. PHPStan custom rules work as a lighter-weight alternative if you don’t want a separate tool in the pipeline. Either way, run the check in CI, not just locally — a rule a developer can skip is not a boundary.
What Performance and Cost Numbers Should You Actually Expect?
You should expect a well-built modular monolith to match or beat an over-decomposed microservices setup on both latency and infrastructure cost, because you eliminate the network hop, serialization, and gateway overhead on every internal call. The two 2026 migration case studies above showed 5-10x latency improvements and 60-90% infrastructure cost reductions after consolidating back into a single deployable unit.
Academic benchmarking backs this up structurally: research comparing microservices and monolith performance found the performance gap widens as service granularity increases, meaning the more aggressively you split a system, the more you pay in inter-service communication overhead for boundaries that add no business value.
My recommendation: track p95 response time and monthly compute spend before and after any decomposition decision. If a proposed microservice split can’t show a concrete number improving, don’t make the cut.
When Should You Still Choose Microservices Instead of a Modular Monolith?
You should choose microservices only when a specific module has a proven, measured bottleneck — it needs independent scaling, a separate deployment cadence, or genuine isolation for blast-radius reasons — not by default. The 2026 consensus among experienced architects is to start monolithic and decompose only when the monolith’s coupling becomes a real, demonstrated bottleneck, not preemptively.
Good candidates for extraction: a video transcoding pipeline with wildly different CPU/memory needs than the rest of the app, or third-party webhook ingestion that you want isolated so a flaky vendor can’t take down checkout. Bad candidates: splitting “Users” and “Orders” into separate services on day one of a project with three engineers, because a whiteboard diagram said so.
My recommendation: keep a running list of modules that have hit a measured bottleneck. When one module is on that list for more than a quarter, that’s your signal to extract it — not before.
How Do You Migrate an Existing Tangled Monolith Into Modules?
You migrate incrementally by carving out one bounded context at a time behind an enforced interface, without touching the database schema until that module’s code boundary is stable. This is the Strangler Fig pattern applied inside a single codebase instead of across services.
- Pick the module with the fewest existing cross-cutting dependencies as your first extraction target — not the biggest or most important one.
- Introduce a
Contracts/interface and route all existing callers through it, even before moving any code behind it. - Split the database tables that module owns into their own schema, replacing any direct joins with calls through the contract.
- Turn on a Deptrac (or PHPStan) rule for that module only, confirm CI catches a deliberate violation, then repeat for the next module.
Expect this to take longer on paper than a big-bang rewrite, and expect it to actually ship — unlike most big-bang rewrites.
| Option | Performance | Complexity | When to Use |
|---|---|---|---|
| Layered (traditional) monolith | Fastest in-process calls, no network hops | Low upfront, rises fast as the codebase grows | MVPs and small teams under ~10 engineers |
| Modular monolith | Near-monolith speed; 5-10x faster than over-split microservices in 2026 case studies | Moderate — needs enforced boundaries (Deptrac/PHPStan) or it decays | Most production teams, 10-150 engineers, single deploy pipeline |
| Microservices | Network + serialization overhead per call; gap widens with finer granularity | High — distributed tracing, service mesh, per-service CI/CD | Proven bottlenecks: independent scaling, separate teams/timezones, isolation for blast radius |
The NexGismo verdict: Default to a modular monolith, not microservices — I’ve now watched two separate teams pay six figures a year to relearn this the hard way. Draw module boundaries around business capabilities, enforce them with Deptrac or PHPStan from day one, and only extract a module into its own service when you have a specific, measured bottleneck a bigger EC2 instance can’t fix. If you’re building on Laravel or Symfony in 2026, the modular monolith isn’t a compromise — it’s the correct default architecture for at least your first 100,000 users.
- 42% of organizations that adopted microservices had consolidated some services back into larger deployable units by Q1 2026, according to CNCF survey data.
- One team that merged 200 microservices back into a single modular monolith cut response times from 2.3 seconds to 0.4 seconds and AWS spend from $800,000 to $190,000 per year.
- A separate migration case study recorded a 10x performance gain, with response times dropping from 1.2 seconds to 89 milliseconds and AWS costs falling 87%.
- Shopify’s core platform handled a peak of 30TB of traffic per minute during Black Friday 2025 on a modular, single-deployable architecture.
- Deptrac and PHPStan can enforce module boundaries in PHP by failing the build when one module imports another module’s internal classes.
- A modular monolith’s modules must never query another module’s database tables directly — every cross-module interaction goes through a public interface or a domain event.
Frequently Asked Questions
What is a modular monolith?
A modular monolith is a single deployable application split into isolated, business-domain-based modules that each own their own data and logic, communicating through enforced interfaces instead of network calls. It combines the operational simplicity of a monolith with the boundary discipline microservices promise but rarely deliver cleanly.
What is the difference between a modular monolith and microservices?
The core difference is deployment: microservices turn a modular monolith’s internal module boundaries into independently deployed network services, while a modular monolith keeps those same boundaries in-process. Microservices add network latency, serialization, and distributed-tracing overhead that a modular monolith avoids entirely.
Why are companies moving back to modular monoliths in 2026?
Companies are moving back because the operational cost of microservices — debugging complexity, network overhead, and per-service infrastructure — outweighed the scaling benefits for most of them. CNCF’s Q1 2026 survey found 42% of prior microservices adopters had consolidated services into larger units, most commonly modular monoliths.
What are the biggest challenges of a modular monolith?
The biggest challenge is boundary erosion: without an automated enforcement tool like Deptrac or PHPStan, developers eventually import another module’s internal classes as a shortcut, and the boundaries quietly dissolve. A modular monolith also can’t scale or deploy individual modules independently, which becomes a real limit at extreme traffic volumes.
When should you break a monolith into microservices?
You should break out a microservice only when a specific module has a proven, measured bottleneck — independent scaling needs, a separate deployment cadence, or strict isolation for blast-radius reasons — not preemptively. The 2026 consensus among architects is to start monolithic and extract a service only when coupling becomes a real, demonstrated problem.
Modular monolith architecture isn’t a retreat from good engineering — it’s what most of the industry now quietly agrees is the right default after a decade of over-decomposing systems that didn’t need it. The boundary discipline you’d apply to microservices still matters; you’re just enforcing it with Deptrac and interfaces instead of a service mesh and a network hop. If you’re starting a new PHP project in 2026, or untangling one that got split too early, draw your modules around business capabilities, give each one exactly one public entry point, and automate the check that keeps other modules from reaching in. Extract a real microservice later, if and when you have the traffic numbers to justify it — not before.