For years, enterprise software teams took it for granted that modern, scalable systems had to be split into microservices. Between 2024 and 2026, the industry corrected course. While distributing systems can help with autonomous deployments and organizational scale, it also adds an operational burden that many workloads and teams cannot justify. Because of this, more engineering leads are choosing modular monoliths.
A modular monolith is not a return to messy legacy codebases. It is a structured architecture that pairs the simpler deployments and speed of a single process with the clear domain boundaries, loose coupling, and data isolation typical of decoupled systems. At Lidex Soluções Digitais, we recommend starting with a modular monolith for custom enterprise software. This avoids the overhead of distributed systems early on while leaving the door open to pull out services later if traffic and scale require it.
The microservices hangover: why enterprise teams are consolidating systems
The shift away from default microservices is supported by industry data. CNCF architectural surveys from 2025 and 2026 report that more than 40% of organizations that adopted microservices later merged parts of their distributed systems back into larger deployable units, pointing to cloud sprawl and maintenance overhead. In addition, over 80% of surveyed engineering leads reported that microservices create more friction than value for teams with fewer than 10 to 15 developers.
Teams that split their systems too early run into predictable problems:
- Network latency and serialization: Swapping in-process calls (measured in nanoseconds) for HTTP or gRPC requests (measured in milliseconds) adds latency across deep call graphs.
- Partial failures: Crossing network boundaries forces teams to configure circuit breakers, retry logic, and distributed tracing to handle network drops and timeouts.
- Distributed state complexity: Maintaining transaction boundaries across independent services requires mechanisms like two-phase commits, sagas, or distributed locks.
This shift is not limited to smaller teams. Late in 2023, Amazon Prime Video consolidated its video-monitoring pipeline from AWS Lambda, Step Functions, and S3 into a single-process service, cutting infrastructure costs by 90% and simplifying how they handled failures.
Shopify runs millions of merchant storefronts on an application with more than 2.8 million lines of code. During the Black Friday and Cyber Monday weekend in 2025, Shopify processed $14.6 billion in GMV, handled peak traffic of 489 million requests per minute, and moved 90 petabytes of data. They ran that workload on a modular monolith backed by partitioned database pods. Running at high scale does not require microservices; it requires clear boundaries and disciplined data modeling.
Enforcing domain boundaries with automated fitness functions
Monoliths did not gain a poor reputation because of how they deployed, but because they easily degraded into tangled code where domains directly imported and modified data from unrelated parts of the system.
Modern modular monoliths prevent this by turning architectural rules into automated tests run during continuous integration (CI). Boundary checks no longer depend on peer review alone; the build pipeline verifies them automatically.
// Enforcing architectural boundaries in .NET via NetArchTest
Types.InAssembly(DomainAssembly)
.That().ResideInNamespace("Lidex.Orders")
.ShouldNot().HaveDependencyOn("Lidex.Billing")
.Check(ArchitectureRules);
Several tools help enforce these rules across different tech stacks:
- Spring Modulith and ArchUnit (JVM): In versions 1.3 and 1.4 (released across 2025 and 2026), Spring Modulith treats root packages as domain modules. Static checks and runtime rules ensure that outside code can only touch a module's public API, while sub-packages stay internal. It generates verification tests using
@ApplicationModuleTestsand exports documentation like AsciiDoctor files and C4 module diagrams. In CI, ArchUnit breaks the build whenever unauthorized cross-module dependencies appear. - NetArchTest and ArchUnitNET (.NET): These libraries let developers write unit tests that inspect assembly metadata. In typical enterprise .NET setups, modules are split into
Module.Domain,Module.Infrastructure, andModule.Contracts. Other modules can only reference.Contracts(which holds public DTOs, interfaces, and events). Any direct reference to domain internals fails the test suite. - Packwerk (Shopify / dynamic languages): Shopify open-sourced Packwerk to run static analysis on Ruby code. It checks boundaries and tracks dependency graphs between packages, showing that dynamic languages can enforce strict domain separation as well.
Database isolation without distributed transactions
Code boundaries fail if modules query each other's database tables directly. When tables are shared across domains, hidden schema dependencies make the code hard to change independently.
The standard approach in enterprise systems is the schema-per-module design within a shared relational database:
- Dedicated schemas: The database is split into logical schemas (such as
orders.*,billing.*, andinventory.*), with each module owning its own schema. - No cross-schema joins: Cross-schema foreign keys (
REFERENCES) and direct SQL joins across schemas are forbidden. Communication between modules runs through defined interfaces. - Local ACID transactions: Changes inside a single module use standard database transactions, avoiding the need for distributed transaction coordinators.
+--------------------------------------------------------------+
| Single Relational Database |
| |
| +-------------------+ In-Process +-------------------+ |
| | orders schema | Domain Event | billing schema | |
| | | ------------> | | |
| | - orders | | - invoices | |
| | - order_items | | - transactions | |
| | - outbox_events | | - outbox_events | |
| +-------------------+ +-------------------+ |
+--------------------------------------------------------------+
To coordinate changes across module boundaries, systems emit in-process domain events (using tools like Spring Events, MediatR, or Wolverine in .NET) alongside the Transactional Outbox pattern.
Spring Modulith includes an Event Publication Registry that records each outgoing event into an outbox table during the local database transaction. After the receiving module finishes processing, the registry marks the event complete. This protects against process crashes without requiring a broker like RabbitMQ or Kafka on day one.
Evolutionary architecture: severability without the upfront tax
Choosing a monolith does not mean an organization is stuck with one deployment forever. A modular monolith provides a clean foundation that can evolve over time.
This characteristic is called severability: the ability to peel off a module with minimal changes to business code. Because modules already interact through explicit interfaces and local events while keeping their database tables separate, the work to extract one is straightforward:
- Stable contracts: Public interfaces and DTOs move to network clients, such as HTTP REST endpoints, gRPC calls, or message queues.
- Independent data: Because the module already lives in its own schema without foreign keys to other tables, that schema can move to a separate database instance without changes to business logic.
- Transport substitution: The in-memory event dispatch or mediator call is replaced with a network or message call, leaving the domain code untouched.
Starting with a modular monolith gives teams time to find product-market fit, refine domain boundaries, and settle on data models before paying to manage multi-service infrastructure.
Operational simplicity and developer ergonomics
Aside from how code is organized, a modular monolith simplifies operations and day-to-day development.
| Metric | Distributed microservices | Enterprise modular monolith |
|---|---|---|
| Deployable artifacts | Multiple container images, Kubernetes manifests (Helm/Kustomize) | Single deployable artifact (Docker container, binary, or JAR/WAR) |
| CI/CD pipelines | One pipeline per service (for example, 20 pipelines for 20 services) | Single pipeline with faster build times |
| Local setup | Requires running local clusters or several background services | In-memory testing without external service dependencies |
| Debugging | Distributed tracing across network hops (such as OpenTelemetry) | Local stack traces inside a single running process |
| Operations | High complexity (service meshes, multi-service releases) | Low complexity (single process, atomic releases) |
Local environment setup directly affects how quickly engineers can ship code. Onboarding developers to a modular monolith usually takes days instead of weeks, because they do not have to run dozens of connected microservices locally.
Debugging is also more predictable. When an error happens, the stack trace stays inside one process, which reduces the immediate need for distributed tracing systems to track down normal bugs. Developers can spend their time on business logic instead of keeping distributed deployment scripts working.
Building for long-term enterprise scalability
Software architecture comes down to managing trade-offs, keeping operating costs reasonable, and helping engineering teams move quickly. For many enterprise systems, jumping to microservices too early adds operational headaches before the business ever feels the scaling advantages.
A modular monolith offers the clean boundaries and modularity of decoupled systems alongside the straightforward operations and performance of a single running process. At Lidex, we help teams design and modernize systems on sound domain foundations, keeping architectures practical and easy to maintain as organizations grow.