Architecture Patterns

Monoliths and Microservices

● Intermediate ⏱ 13 min read architecture

The choice between a monolithic and a microservices architecture is one of the most consequential decisions in system design — and one of the most frequently misunderstood. Microservices are not inherently superior. The right choice depends on team size, product maturity, scale requirements, and operational capability. Both architectures have legitimate use cases, and the path between them is a deliberate evolution, not a one-time migration.

The Monolith

A monolith is a single deployable unit containing all application functionality. The user interface, business logic, and data access code are packaged and deployed together.

Types of monoliths:

Advantages of monoliths:

Monolith vs microservices: a single deployable unit vs independently deployed services communicating over the network

Microservices

A microservices architecture decomposes the application into small, independently deployable services, each responsible for a single bounded context. Services communicate over the network — typically via HTTP/REST, gRPC, or asynchronous messaging.

Core principles:

Advantages of microservices:

Comparison

MonolithMicroservices
DeploymentSingle artifactMany independent services
ScalingScale the whole appScale individual services
CommunicationIn-process function callsNetwork (HTTP, gRPC, messaging)
DataShared databaseDatabase per service
TransactionsACID across all modulesSaga / 2PC for cross-service
Operational complexityLowHigh (service mesh, tracing, discovery)
Team structureWorks well with small teamsRequires many autonomous teams
Development speed (early)FastSlow (infrastructure overhead)
Development speed (at scale)Slow (coordination overhead)Fast (team autonomy)

When to Split

The most common mistake in microservices adoption is splitting too early. A team of 5 engineers does not need 20 microservices. The infrastructure overhead alone — CI/CD pipelines, service discovery, distributed tracing, container orchestration — will consume more engineering time than the feature work it was meant to enable.

Signs that a monolith is ready to be split:

Split along bounded contexts — the natural domain boundaries of your business. Don’t split by technical layer (a "database service" or a "utilities service" is not a microservice — it’s a shared library that happens to live behind an HTTP call).

The Strangler Fig Pattern

The Strangler Fig pattern (named after the strangler fig tree, which grows around a host tree and eventually replaces it) is the standard approach for extracting microservices from a monolith incrementally:

  1. Identify the capability to extract. Choose a bounded context with clear inputs and outputs, minimal cross-dependencies, and a measurable reason to split (bottleneck, team autonomy, compliance).
  2. Build the new service alongside the monolith. Implement the capability in the new service. Don’t remove it from the monolith yet.
  3. Route traffic to the new service. Use an API gateway or reverse proxy to send requests for the extracted capability to the new service. The monolith handles everything else.
  4. Migrate data. Move the capability’s data to the new service’s database using the dual-write migration approach (see the Database Federation guide).
  5. Remove from the monolith. Once the new service handles all traffic, delete the code and data from the monolith.

This approach is incremental and reversible. At each step, you can stop or roll back without a catastrophic migration failure.

Common Pitfalls

Distributed monolith: Services that must be deployed together, share a database, or call each other synchronously in long chains are not really microservices — they’re a monolith with network calls added. All the operational complexity of microservices, none of the independence. This is usually caused by splitting along technical layers rather than domain boundaries.

Chatty services: Services that make dozens of synchronous calls to other services per request. Each network hop adds latency and a failure point. If Service A calls B, which calls C, which calls D for a single user request, a slowdown in D cascades to A’s response time. Use asynchronous messaging, BFF aggregation, or service consolidation to reduce synchronous chains.

No service contract versioning: Changing a service’s API without versioning breaks consumers. Use versioned endpoints (/v2/orders) or schema evolution (Protobuf field additions), and maintain backward compatibility for at least one version cycle.

Underestimating operational complexity. Microservices require container orchestration (Kubernetes), service discovery, distributed tracing (OpenTelemetry, Jaeger), centralized logging, and per-service alerting. Teams that adopt microservices without investing in this infrastructure end up with unmaintainable systems.

Design Considerations