For the better part of a decade, the software industry treated microservices as the default architecture for any serious application. Monoliths were considered legacy, a sign that your team had not kept up with modern practices. In 2026, the pendulum has swung back, and the industry consensus is more nuanced. Both architectures have their place, and the choice between them should be driven by your specific constraints, not by fashion.
At Pepla, we have built and maintained systems on both ends of the spectrum. We have decomposed monoliths into microservices for clients who genuinely needed it, and we have consolidated microservice sprawl back into modular monoliths for clients who did not. This article shares the framework we use to make that decision.
The Monolith-First Approach
Martin Fowler's "MonolithFirst" advice from 2015 has aged remarkably well. The argument is simple: start with a monolith, establish clear module boundaries within it, and extract services only when you have a proven need. The reasoning is sound:
A modular monolith preserves the option to decompose later -- with the benefit of hindsight about real boundaries.
- You do not know where the service boundaries should be at the start of a project. Getting boundaries wrong is expensive with microservices and cheap with a monolith.
- A well-structured monolith is easier to develop, test, deploy, and debug than a distributed system.
- The operational overhead of microservices (service discovery, distributed tracing, message brokers, container orchestration) is substantial and fixed, regardless of how many services you have.
- Premature decomposition creates distributed monoliths: systems that have all the complexity of microservices with none of the benefits.
A modular monolith with clear boundaries is not technical debt. It is a pragmatic architectural choice that preserves the option to decompose later, with the benefit of hindsight about where the real boundaries are.
What a Good Monolith Looks Like
A well-structured monolith is not a big ball of mud. It has clear internal module boundaries, enforced through the type system, package structure, or architectural fitness functions. Each module owns its data, exposes a defined interface, and does not reach into another module's internals. The fact that these modules happen to run in the same process does not mean they are coupled.
Languages and frameworks have caught up with this approach. .NET's modular monolith patterns, Java's module system (JPMS), and Go's internal package convention all provide mechanisms for enforcing boundaries within a single deployment unit.
When Microservices Add Genuine Value
Microservices solve specific problems. If you do not have these problems, they add complexity without benefit:
- Independent deployment: Different parts of your system have radically different release cadences. Your payment processing service changes weekly but your reporting service changes quarterly. Independent deployment lets teams ship without coordinating releases.
- Independent scaling: One component has fundamentally different resource requirements. Your image processing pipeline needs GPUs while your API layer needs network I/O. Different scaling profiles justify different deployment units.
- Technology diversity: Different problems genuinely require different technology stacks. Your ML pipeline runs Python while your API runs Go. A monolith forces a single stack.
- Organisational autonomy: Multiple autonomous teams need to work on the same system without stepping on each other. Conway's Law makes this a legitimate driver of decomposition.
- Fault isolation: A failure in one component should not take down the entire system. If your search index crashes, users should still be able to browse and purchase.
Notice that none of these drivers are about code quality, testability, or "modern architecture." A monolith can be perfectly testable and well-architected. If you are reaching for microservices to solve code quality problems, you are solving the wrong problem with the wrong tool.
Start monolith-first and extract services only when the complexity tax is clearly justified.
The Complexity Tax of Distributed Systems
Every network call introduces failure modes that do not exist in a function call. This is not theoretical; it is the daily reality of operating microservices.
The Eight Fallacies of Distributed Computing
Peter Deutsch's fallacies, written in 1994, remain brutally relevant:
- The network is reliable (it is not, especially in South African data centres)
- Latency is zero (it is not, and it compounds across service calls)
- Bandwidth is infinite (it is not, especially with serialisation overhead)
- The network is secure (it is not, and every service-to-service call needs authentication)
- Topology does not change (it does, especially in containerised environments)
- There is one administrator (there is not when each team owns their services)
- Transport cost is zero (it is not, both financially and computationally)
- The network is homogeneous (it is not, particularly in multi-cloud environments)
Each of these fallacies translates to concrete engineering work: retry logic, circuit breakers, timeout configuration, distributed tracing, service mesh management, and more. This work does not ship features. It is the tax you pay for distribution.
Communication Patterns: Synchronous vs Asynchronous
How services communicate is as important as how they are decomposed.
Synchronous Communication (REST, gRPC)
Synchronous calls are simple and intuitive. Service A calls Service B and waits for a response. The problems emerge under load and failure:
- Cascading failures: If Service B is slow, Service A's threads are blocked waiting, which makes Service A slow, which blocks Service C, and so on. One slow service can take down the entire system.
- Temporal coupling: All services in the call chain must be available simultaneously. Downtime in any service breaks the entire flow.
- Latency accumulation: Each hop adds latency. A request that touches five services, each adding 50ms, has a baseline latency of 250ms before any of them do real work.
Asynchronous Communication (Events, Message Queues)
Asynchronous patterns decouple services in time. Service A publishes an event and moves on. Service B processes it when ready. This eliminates cascading failures and temporal coupling, but introduces its own challenges:
- Eventual consistency: Data across services is not immediately consistent. Users may see stale data during the propagation window.
- Message ordering: Events may arrive out of order. Your system must be designed to handle this or enforce ordering at the transport level.
- Debugging complexity: Tracing a request through asynchronous event chains is significantly harder than following synchronous call stacks.
- Idempotency: Messages may be delivered more than once. Every consumer must handle duplicate processing gracefully.
Our recommendation at Pepla: use synchronous communication for queries (reads) and asynchronous communication for commands (writes). This pattern, sometimes called CQRS-lite, gives you the simplicity of synchronous reads with the resilience of asynchronous writes.
Architecture follows organisation -- align service boundaries with team boundaries.
Data Ownership and the Database Question
In a monolith, all modules share a database. In microservices, each service owns its data and exposes it only through its API. This is perhaps the most impactful difference between the two architectures.
Shared databases are simple but create tight coupling. A schema change in one module can break another. Data integrity is maintained through database transactions, which is straightforward. Reporting and analytics can query across all data directly.
Service-owned databases provide autonomy but create complexity. Cross-service queries require API calls or data replication. Distributed transactions across services are notoriously difficult and best avoided. The saga pattern provides eventual consistency for multi-service operations, but it is significantly more complex than a database transaction.
For many projects, a middle ground works well: a shared database with schema-level isolation. Each module owns its schema and accesses other modules' data only through defined views or APIs. This provides most of the autonomy benefits without the operational complexity of multiple database instances.
Conway's Law: Architecture Follows Organisation
Melvin Conway observed in 1967 that organisations produce system designs that mirror their communication structures. This is not just an observation; it is a force. If you have a single team, a monolith is the natural architecture. If you have multiple autonomous teams, microservices align with their organisational boundaries.
The inverse Conway manoeuvre, deliberately structuring teams to produce a desired architecture, is a powerful technique. If you want microservices, organise teams around service boundaries. If you want a modular monolith, organise teams around feature areas with shared codebase ownership.
Trying to run microservices with a monolithic team structure (one team responsible for all services) gives you the worst of both worlds: distributed complexity with centralised bottlenecks.
Architecture should follow team structure -- microservices with a monolithic team gives you the worst of both worlds.
A Decision Framework
Based on our experience delivering both architectures, here is when we recommend each:
Start with a modular monolith when:
- You are building a new product and the domain boundaries are not yet clear
- Your team is fewer than 15-20 developers
- You can deploy the entire system together without coordination problems
- Your scaling requirements are uniform across components
- Time to market is the primary constraint
Consider microservices when:
- You have multiple autonomous teams that need to ship independently
- Different components have fundamentally different scaling or technology requirements
- You have the operational maturity to manage distributed systems (container orchestration, observability, CI/CD per service)
- The domain boundaries are well understood and stable
- You can afford the operational overhead (it is substantial and ongoing)
The right architecture is the one that lets your team ship reliable software efficiently. Not the one that looks best on a conference slide.




