Microservices vs. Monolith: Choosing the Right Architecture
A monolith optimizes for simplicity: one codebase, one deployment, straightforward debugging, and real database transactions across your whole domain. Microservices optimize for independent deployability and team autonomy, at the cost of distributed-systems complexity - service discovery, distributed tracing, eventual consistency, and container orchestration. Most teams should start with a well-structured monolith, ideally a modular one with clean internal boundaries, and split out services only when a specific, demonstrated need arises: one component that must scale independently, or a team that needs its own deploy cadence. Splitting by default, before either pressure exists, tends to produce a distributed monolith - all the operational overhead of microservices with none of the independence benefits.
Key takeaways
- A monolith optimizes for deployment simplicity, easy debugging, and native transactional consistency; microservices optimize for independent deployability and team autonomy.
- Microservices require real operational maturity - service discovery, distributed tracing, centralized logging, container orchestration - that many teams underestimate until they are already committed.
- Conway's Law means the architecture question is partly an org-design question: microservices tend to map to team boundaries, not the other way around.
- The 'distributed monolith' anti-pattern - services that are deployed separately but still tightly coupled - delivers all the operational cost of microservices and none of the benefit.
- A modular monolith is a genuine middle ground: well-separated internal modules in one deployable unit, with the option to extract services later once boundaries are proven.
At a glance
| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment unit | Single | Multiple, independent |
| Deployment simplicity | High | Lower - coordination needed across service versions |
| Debugging | In-process call stack | Requires distributed tracing across services |
| Data consistency | Native ACID transactions | Sagas or eventual consistency required |
| Independent scaling of components | No | Yes |
| Operational infrastructure needed | Minimal | Service discovery, tracing, orchestration, centralized logging |
Most teams should start with a well-structured, ideally modular monolith and split into microservices only once a specific, demonstrated need arises - one component that must scale independently, or a team that needs its own deploy cadence - since splitting by default tends to produce a distributed monolith with microservices' operational cost and none of its benefit.
Every architecture decision is a bet on what will hurt more: the pain of change, or the pain of scale. The microservices-versus-monolith question gets asked constantly and answered dogmatically, both directions, which is part of why so many teams get it wrong. The honest answer is that neither architecture is intrinsically stronger. Each one optimizes for a different set of pressures, and the right choice depends on which pressures your team and your system actually face today - not the ones you expect to face in three years, and not the ones a job posting or a conference talk made sound inevitable.
This article works through what each architecture actually optimizes for, the organizational dimension that most technical comparisons skip entirely, the failure modes that show up when teams adopt microservices without the operational foundation to support them, and a practical framework for making the call on a real system.
What a Monolith Actually Optimizes For
A monolith is a single deployable unit: one codebase, one build pipeline, one running process (or a small, uniformly-scaled fleet of identical processes) serving the whole application. That structure isn't a limitation to be engineered around by default - it's a deliberate tradeoff, and understanding what it buys you is the starting point for any honest comparison.
Deployment simplicity. There is one thing to build, one thing to test, one thing to deploy, and one thing to roll back. A release either goes out or it doesn't; there's no coordination problem of "service A needs version 2 of the shared contract before service B can ship its consumer." Rollback means reverting to the previous build artifact, not untangling which of six services need to revert together.
Debugging and local reasoning. A stack trace in a monolith runs through function calls in a single process. When something breaks, an engineer can generally set a breakpoint, step through the call chain, and see the entire path from HTTP handler to database query in one debugger session, on one machine, without correlating logs across a dozen processes. This matters more than it sounds like it should - most production incidents are debugged under time pressure, and the cognitive overhead of tracing a request across a network is a real cost, not a theoretical one.
Transactional consistency. A relational database transaction gives you atomicity across everything it touches. If an order-placement flow needs to update inventory, create an order record, and adjust a customer's loyalty balance, a monolith with a single database can wrap all three in one transaction: either all three commit, or none do. There is no intermediate state where inventory is decremented but the order was never created. This guarantee is easy to take for granted until you have to build the same correctness properties by hand across service boundaries.
Lower baseline operational overhead. A monolith run as a small number of identical instances behind a load balancer needs a fraction of the infrastructure that a microservices estate needs: no service registry, no inter-service authentication scheme, no need to trace a request across process boundaries, no per-service on-call rotation. A small team can run a monolith in production with a modest DevOps investment.
None of this means monoliths don't have real weaknesses. A large, poorly-organized monolith becomes a place where every change risks breaking something unrelated, where a single team's deploy schedule gatekeeps everyone else's releases, and where scaling means scaling the whole application even if only one code path is under load. Those weaknesses are exactly what microservices are designed to address - but they are not automatic consequences of choosing a monolith. They are consequences of choosing a poorly-organized monolith, which is a different problem with a different fix (covered below, in the section on modular monoliths).
What Microservices Actually Optimize For
Microservices decompose a system into independently deployable services, each typically owning its own data store, communicating over the network (usually HTTP/REST, gRPC, or asynchronous messaging), and each capable of being built, tested, deployed, and scaled without coordinating with the others.
Independent deployability. This is the core value proposition, and everything else follows from it. If the recommendations service can ship ten times a day while the billing service ships once a week, in a microservices architecture that's just two independent release schedules. In a monolith, it's one shared schedule, and the slower, higher-risk component sets the pace for everyone.
Independent, targeted scaling. If one component of a system is disproportionately expensive - say, an image-processing pipeline that needs GPU instances while the rest of the application is CPU-bound and cheap to run - a microservice for that component can be scaled and provisioned on its own, without over-provisioning the entire application to match its most demanding part.
Team autonomy and reduced coordination overhead. A team that owns a service end to end - its code, its data store, its deploy pipeline, its on-call rotation - can make internal changes without asking another team to review or coordinate. This matters more as organizations grow: past a certain number of engineers, coordinating changes to a single shared codebase becomes a real tax on velocity, and clear service ownership is one way to remove that tax.
Fault isolation, in principle. A well-isolated service that crashes or degrades doesn't necessarily take down the rest of the system, provided the calling services handle timeouts and failures gracefully. This is a real benefit, but it's conditional - it only holds if the failure-handling work (circuit breakers, timeouts, retries, graceful degradation) is actually built, which is additional engineering effort that a monolith doesn't require in the same form.
The costs are equally real, and this is the part that gets underplayed in architecture pitches: every one of these benefits is purchased with distributed-systems complexity that didn't exist in the monolith. A network call can fail in ways a function call cannot. Data that used to live in one transactionally consistent database now lives in several, with no shared transaction across them. And observing a system that spans a dozen independently deployed services requires infrastructure that a monolith never needed to build.
The Operational Prerequisite Most Teams Underestimate
This is the part of the microservices conversation that gets skipped most often, and it's the part that causes the most damage when it's skipped. Microservices are not a code-organization technique you adopt by drawing different module boundaries. They are a bet that your organization already has - or is willing to build, before or alongside the product work - a meaningful amount of distributed-systems operational infrastructure.
At minimum, that includes:
- Service discovery. When you have more than a handful of services, each potentially running multiple instances that come and go with deploys and autoscaling, something needs to track which instances of which service are currently healthy and reachable. Hardcoding IP addresses or hostnames does not survive contact with a real deployment.
- Distributed tracing. A single user-facing request might touch six or eight services on its way to a response. When that request is slow or fails, you need a way to see the entire path it took, and how much time was spent in each hop, or debugging becomes guesswork across disconnected log files.
- Centralized, correlated logging. Logs scattered across dozens of service instances are close to useless unless they're aggregated in one place and can be correlated by a request or trace ID. Without this, "what happened during this incident" becomes an exercise in manually gathering logs from every service that might have been involved.
- Container orchestration. Running many independently deployable services in production, each needing its own scaling, health checks, rolling deploys, and restart behavior, is not something teams do by hand at any meaningful scale. This is what Kubernetes (or a comparable orchestration layer) is for, and operating it well is itself a skill set that takes real time to build.
- Inter-service authentication and network policy. Once services talk to each other over a network rather than through in-process function calls, you need a story for how those calls are authenticated and authorized, and often for how traffic between services is encrypted and controlled.
- On-call and incident response that spans services. When something breaks, someone needs to figure out which of several services is at fault, often under time pressure, with partial information from multiple systems.
None of this is exotic - it's mature, well-documented infrastructure that many organizations run successfully. The mistake is treating it as optional or as something to "figure out as we go." Teams that adopt microservices without this foundation in place typically end up building it reactively, service by service, incident by incident, which is a far more expensive way to acquire the same capability than building it deliberately - or than not needing it at all, because the monolith never required it in the first place.
The honest question to ask before adopting microservices isn't "do we understand the pattern." It's "do we already operate something with this level of operational maturity, or are we prepared to build that capability as a first-class project, not a side effect of splitting the codebase."
Conway's Law: Architecture Is Also an Org Chart
Conway's Law observes that systems tend to mirror the communication structure of the organizations that build them. In practice, this shows up constantly in the microservices decision, and ignoring it is one of the more common ways teams end up with the worst of both worlds.
If an organization has three small teams that talk to each other easily and share context daily, splitting the system into fifteen microservices doesn't produce fifteen independently-operating units - it produces three teams now responsible for coordinating across fifteen services, which is strictly more coordination overhead than three teams sharing one well-organized codebase. The service boundaries didn't match any real organizational boundary, so they didn't remove any real coordination cost; they just added network hops and deployment complexity on top of the coordination that was already happening.
Conversely, if an organization has genuinely independent teams - each responsible for a distinct business capability, each with its own priorities, its own release cadence, and infrequent need to coordinate with the others - service boundaries that map to those team boundaries can remove real friction. Each team ships on its own schedule because the system's structure matches the organization's structure, not despite it.
This means the microservices decision is not purely a technical one. It's partly a question about how your organization is actually structured and how it actually communicates, and a service boundary that doesn't correspond to any real organizational seam usually isn't creating independence - it's creating overhead. Teams considering a split should ask a blunt question: does this service boundary track an actual team boundary, with its own priorities and release cadence, or is it a boundary we drew on a whiteboard that doesn't correspond to how the organization actually works? If it's the latter, the split is unlikely to deliver the autonomy it promises.
The Distributed Monolith: All the Cost, None of the Benefit
The most damaging outcome in this whole space isn't choosing the "wrong" architecture outright - it's ending up with a distributed monolith: a system that has been split into separate, independently-deployed services, but where those services remain so tightly coupled that they can't actually be deployed, scaled, or changed independently. The result is a system that pays the full operational cost of microservices - network calls, service discovery, distributed tracing, container orchestration - while getting none of the independence benefits that were supposed to justify that cost.
A distributed monolith typically develops from one of a few patterns:
- Shared databases across services. If two "independent" services both read and write the same database tables, they are not actually independent - a schema change in one requires coordinating with the other, exactly as it would in a monolith, except now the coordination happens across a network boundary and often across team communication channels instead of in a single code review.
- Synchronous call chains that must all succeed together. If handling a single user request requires service A to call service B, which calls service C, and the whole chain fails if any link fails, the services are coupled at runtime just as tightly as functions in a monolith - but now each hop adds latency, and each hop is a new way for the request to fail that a monolith's in-process function call never had.
- Lockstep releases. If deploying service A always requires deploying a compatible version of service B in the same release window, the two services aren't independently deployable in any meaningful sense, even though they run as separate processes.
- Shared internal libraries that leak implementation details. When services share a library that encodes internal data structures or business logic (rather than a clean, versioned public contract), a change to that shared code still requires coordinated updates across every service that depends on it - the coupling just moved from "same codebase" to "same shared package," without becoming looser.
The way to avoid this isn't more services or better tooling - it's discipline about what independent deployability actually requires: each service owns its data, exposes a stable and versioned contract to consumers, and can be changed, deployed, and rolled back without a coordinated release involving other services. If that's not true, the system is a monolith that happens to be distributed across a network, and it has taken on a monolith's coordination costs plus a distributed system's operational costs, without gaining the benefits either architecture is supposed to offer.
Data Consistency: What You Lose When You Split
One of the least-discussed costs of microservices is what happens to data consistency once a domain that used to live in one database gets split across several. In a monolith, an operation that spans multiple concerns
- placing an order, decrementing inventory, and updating a customer's loyalty points - can be wrapped in a single ACID transaction. It either all happens, or none of it does, and the database enforces that guarantee for you.
Once those concerns live in separate services with separate data stores, that guarantee is gone. There is no single transaction that spans a network boundary between two independently-owned databases. Two broad patterns fill the gap, and both require deliberate design work that a monolith simply doesn't ask for:
The saga pattern. A distributed operation is modeled as a sequence of local transactions, one per service, each with a defined compensating action to undo its effect if a later step in the sequence fails. If placing an order involves reserving inventory, charging payment, and scheduling shipment, and the payment step fails, the saga triggers a compensating action to release the inventory reservation. This works, but it means the team has to explicitly design and test the failure and compensation path for every multi-service operation - work that a single database transaction handled for free.
Eventual consistency via asynchronous events. Rather than trying to keep every service perfectly in sync in real time, services publish events when their own state changes (an order was placed, a payment was captured), and other services consume those events to update their own view of the world, accepting that there will be a window - usually milliseconds to seconds, but sometimes longer under load or failure - during which different parts of the system disagree about the current state. This is workable for a great many business processes, but it requires the team, and often the product and business stakeholders, to be explicit about which parts of the system can tolerate that window and which cannot.
Both patterns are well-understood and used successfully in production systems everywhere. The point isn't that they don't work - it's that they are additional design and engineering surface area that doesn't exist in a monolith, and teams that don't plan for this upfront tend to discover it midway through an incident, when two services disagree about whether an order was actually paid for.
The Modular Monolith: A Genuine Middle Ground
Between "one undifferentiated codebase" and "a dozen independently deployed services" sits an option that gets far less attention than it deserves: the modular monolith. It's a single deployable application, internally organized into modules with explicit, enforced boundaries and interfaces - each module owns its own data access and internal logic, and other modules interact with it only through a defined interface, not by reaching into its internals or its database tables directly.
This is not simply "a monolith with folders." A modular monolith takes the same discipline that good service design requires - clear ownership, explicit contracts, no reaching across boundaries - and applies it inside a single process and a single deployment, without paying for network calls, service discovery, or separate data stores.
The benefits are substantial:
- It keeps a monolith's deployment simplicity and transactional consistency. One deploy, one rollback, and database transactions that can still span module boundaries when needed, because the modules share a database (even if each module logically owns its own tables).
- It makes team ownership boundaries explicit even without separate deployments. Teams can own a module and its interface without needing a service boundary to enforce that ownership.
- It makes a future extraction into real services dramatically cheaper, if and when that becomes necessary. If a module already has a clean interface and doesn't reach into other modules' internal data, turning it into a standalone service later is a matter of replacing an in-process call with a network call behind the same interface - not a ground-up redesign of the domain's boundaries. Many teams that jump straight to microservices spend their first year discovering where the domain boundaries actually are; a modular monolith lets you discover those boundaries while still able to refactor them cheaply, because everything is still in one codebase.
- It avoids paying for operational infrastructure the team doesn't yet need. No service discovery, no distributed tracing, no container orchestration platform to operate, until a real, demonstrated need justifies the investment.
The discipline required to keep a modular monolith modular - not letting modules quietly couple to each other's internals over time - is real and has to be actively maintained, usually through a combination of code organization, dependency-direction rules enforced in the build or linter, and code review norms. But that discipline is far cheaper to maintain than the discipline required to keep a distributed system from becoming a distributed monolith, and it fails more gracefully: a coupling violation inside a modular monolith is a code review comment, not a production incident spanning three services.
Microservices vs. Monolith vs. Modular Monolith: A Comparison
| Dimension | Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deployment unit | Single | Single | Multiple, independent |
| Deployment simplicity | High | High | Lower - coordination needed across service versions |
| Debugging | In-process call stack | In-process call stack | Requires distributed tracing across services |
| Data consistency | Native ACID transactions | Native ACID transactions across modules | Sagas or eventual consistency required |
| Independent scaling of components | No | No | Yes |
| Independent team deploy cadence | No | Partial - enforced via code ownership, not deploys | Yes |
| Operational infrastructure needed | Minimal | Minimal | Service discovery, tracing, orchestration, centralized logging |
| Risk of becoming a "distributed monolith" | Not applicable | Low | Real, if boundaries aren't enforced |
| Cost of getting domain boundaries wrong early | Low - refactor in one codebase | Low - refactor in one codebase | High - requires re-architecting deployed services and their contracts |
| Strongest fit for team size/structure | Small to mid-size, single team | Small to mid-size, single team planning to grow | Multiple independent teams with distinct release cadences |
A Practical Decision Framework
Rather than starting from "which architecture is more modern," a more useful starting point is three concrete questions about the system and the organization as they actually are today.
1. How many genuinely independent teams need to ship on different schedules? If there's one team, or a small number of teams that routinely need to coordinate closely, independent deployability isn't solving a problem you have. If there are several teams that own distinct business capabilities and regularly find a shared release train slowing them down, that's a real signal in favor of splitting along those existing team boundaries.
2. Does any specific component have scaling or resource needs wildly different from the rest of the system? A component that's genuinely resource-different - CPU-bound versus I/O-bound, GPU-dependent versus not, subject to a load pattern the rest of the system doesn't share - is a concrete, technical argument for extracting it into its own service, independent of any organizational consideration.
3. What operational capability does the team actually have today, not what it could build eventually? If distributed tracing, centralized logging, service discovery, and container orchestration aren't already in place and well understood, adopting microservices means building all of that as a parallel, first-class effort alongside the product itself. That can be the right call, but it should be a deliberate, budgeted decision, not something absorbed silently into a project timeline.
If the honest answers are "one team," "no meaningfully different scaling needs," and "limited distributed-systems operational experience," the right starting point is a monolith - and a modular one, so the option to split later stays open and cheap. If a team already has multiple independent groups with distinct release needs, a component with genuinely different scaling requirements, and an existing operational foundation for running distributed systems, microservices are a reasonable and often strong fit.
The failure mode to avoid in both directions is adopting an architecture because of where the industry conversation currently sits rather than because of a demonstrated need in front of you. A monolith that's grown unwieldy because no one enforced internal module boundaries is a real problem, but the fix is usually introducing that discipline - a modular monolith - not necessarily a wholesale move to distributed services. And a microservices architecture adopted before any team boundary or scaling need required it tends to produce exactly the distributed monolith described earlier: every operational cost of a distributed system, purchased without the independence it was supposed to buy.
Migrating Later, Once the Need Is Real
None of this argues that microservices are wrong for the systems that genuinely need them - large organizations with many independent product teams run substantial microservices estates successfully, and for good reason: the alternative, a single shared codebase gatekeeping a dozen teams' release schedules, would cost them more. The point is sequencing.
A well-structured, modular monolith is not a temporary embarrassment on the way to "real" architecture - it's a legitimate long-term choice for a large share of systems, and for the ones that do eventually need to split, it's the strongest possible starting position. Clean module boundaries, each module's own data access already isolated, and interfaces that don't leak internal detail make a later extraction a targeted, well-scoped project: pick the module with the clearest boundary and the strongest demonstrated need, replace its in-process interface with a network one, give it its own data store, and stand up the operational tooling that specific service needs. That is a fundamentally different, and far less risky, undertaking than a ground-up rewrite into a dozen services based on boundaries nobody has validated yet.
The architecture question, in the end, isn't "monolith or microservices" as a permanent, once-only choice. It's a sequencing question: build the system so its internal boundaries are clean enough that the architecture can evolve when a specific, demonstrated need justifies the added operational complexity - and resist the pressure to pay that complexity cost upfront, before that need exists.
Frequently asked questions
Should a new product start with microservices?
Almost never. Early on, the domain boundaries you'd split along are still unclear, and a new product typically has one team, not several that need independent deploy cadences. A well-structured monolith lets the team move fast, keep transactions simple, and observe where real seams form in the domain. Splitting before boundaries are proven usually means re-drawing service boundaries later anyway, after paying the operational cost of running them as separate services in the meantime.
What is a distributed monolith, and how do you know if you have one?
A distributed monolith is a system split into separate services that still can't be deployed independently - a change to one service routinely forces coordinated redeploys of others, or the services share a single database and step on each other's schema. Signs include synchronous call chains that must all succeed together, shared libraries that couple internal implementation details across service boundaries, and release processes that version and ship every service together. If services can't fail, scale, or deploy on their own timeline, splitting the code didn't actually decouple the system.
What operational capabilities does a team need before adopting microservices?
At minimum: centralized, correlated logging across services; a distributed tracing system so a single request can be followed across process boundaries; service discovery and health checking so instances can be added, removed, or replaced without manual reconfiguration; and enough container orchestration experience (typically Kubernetes) to handle rolling deploys, restarts, and scaling without an engineer manually intervening. If a team doesn't already operate most of this for its current system, adopting microservices means building this operational layer at the same time as the product - a significant, often underestimated cost.
How is data consistency handled across microservices without shared database transactions?
Instead of a single ACID transaction spanning tables, cross-service operations use patterns like the saga pattern - a sequence of local transactions, each with a defined compensating action if a later step fails - or asynchronous, event-driven updates that accept eventual consistency between services. Both require the team to design explicitly for partial failure: what happens if step three of five fails, and how does the system detect and recover from a service being temporarily out of sync with another. This is a real engineering cost that a monolith's local transactions don't impose.
What is a modular monolith, and when does it make sense?
A modular monolith is a single deployable application internally organized into modules with explicit boundaries and interfaces, similar to how services would be separated, but without the network calls, independent deployment, or separate data stores. It makes sense for most teams below a certain size or maturity threshold: it keeps the deployment and debugging simplicity of a monolith while making a later extraction into real services far less disruptive, because the seams are already drawn in the code.
Related reading
- Custom Software DevelopmentDesign and development of secure, scalable custom software for companies across Sweden and the Nordic region.
- DevOps & Cloud InfrastructureDevOps engineering for teams with manual, risky deployments: CI/CD, infrastructure-as-code, and observability built as a real discipline.
- DockerHow North Tech Labs uses Docker for reproducible containers — real strengths, honest trade-offs, and where it fits in a deployment architecture.
- KubernetesHow North Tech Labs uses Kubernetes for container orchestration at scale — real capabilities, real operational cost, and when a simpler approach fits better.
- Node.jsHow North Tech Labs uses Node.js for backend systems and APIs — its real strengths, its trade-offs, and where another runtime fits better.
Have a specific question about your project?
This article covers the general pattern — the fastest way to get a specific answer is to ask us directly.