Monoliths and Microservices
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:
- Modular monolith: The codebase is internally organized into well-defined modules with clear boundaries, but deployed as a single process. This is the best of both worlds for many teams — the simplicity of single-process deployment with the organization of bounded contexts.
- Layered monolith: Organized into horizontal layers (presentation, business logic, data access) but without strong vertical domain boundaries. Common in older enterprise applications.
- Big ball of mud: No clear structure. Everything depends on everything. This is what monoliths become without deliberate architecture — not an intentional design choice.
Advantages of monoliths:
- Simple to develop and test: Everything runs in one process. No network calls between modules, no service discovery, no distributed tracing needed for debugging.
- Simple to deploy: One artifact, one deployment. Rollback is straightforward.
- Low operational overhead: One database, one log stream, one set of metrics to monitor.
- In-process communication: Module-to-module calls are function calls — microsecond latency, not milliseconds. No serialization overhead.
- Easy cross-module transactions: A single database transaction can span all modules. No distributed transaction complexity.
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:
- Single responsibility: Each service owns one business capability — the Order service handles orders, the Payment service handles payments. Changes to one capability require changing only one service.
- Independent deployment: Each service has its own release cycle, deployment pipeline, and versioning. The Payments team can deploy 10 times a day without coordinating with the Orders team.
- Data isolation: Each service owns its own database. No shared databases. (See the Database Federation guide.)
- Decentralized governance: Teams choose the best technology for their service — the Orders service uses PostgreSQL, the Search service uses Elasticsearch, the Recommendations service uses a graph database.
Advantages of microservices:
- Independent scaling: Scale only the services under load. If the Image Processing service is the bottleneck, add instances of that service alone — not the entire application.
- Team autonomy: Small teams own services end-to-end. Conway’s Law states that system architecture mirrors team communication structure. Microservices align architecture to team boundaries.
- Fault isolation: A bug in the Recommendations service doesn’t take down the Order service. Failures are contained within service boundaries (with circuit breakers).
- Technology heterogeneity: Use the right tool for each job. ML workloads in Python, payment processing in Java, real-time features in Go.
Comparison
| Monolith | Microservices | |
|---|---|---|
| Deployment | Single artifact | Many independent services |
| Scaling | Scale the whole app | Scale individual services |
| Communication | In-process function calls | Network (HTTP, gRPC, messaging) |
| Data | Shared database | Database per service |
| Transactions | ACID across all modules | Saga / 2PC for cross-service |
| Operational complexity | Low | High (service mesh, tracing, discovery) |
| Team structure | Works well with small teams | Requires many autonomous teams |
| Development speed (early) | Fast | Slow (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:
- Deployment bottlenecks: One team’s changes frequently delay another team’s releases. Coordination cost of a single deployment exceeds the cost of operating a separate service.
- Scaling asymmetry: One part of the application needs 10x the resources of the rest, and scaling the whole monolith wastes capacity.
- Team size: More than 8–10 engineers working in the same codebase regularly create merge conflicts and coordination overhead.
- Technology mismatch: A new capability genuinely requires a different runtime (Python for ML inference, Go for high-concurrency networking) that cannot be integrated cleanly into the monolith.
- Compliance isolation: A capability handling PCI or HIPAA data benefits from a dedicated, audited service boundary.
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:
- 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).
- Build the new service alongside the monolith. Implement the capability in the new service. Don’t remove it from the monolith yet.
- 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.
- Migrate data. Move the capability’s data to the new service’s database using the dual-write migration approach (see the Database Federation guide).
- 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
- Start with a well-structured monolith. Even if you plan to eventually move to microservices, begin with a modular monolith. The domain boundaries you discover building it become the service boundaries when you split.
- Two-pizza rule. A microservice should be small enough for a team of 6–8 to own, develop, and operate fully. If a service requires more than one team to understand, it’s too large — or it’s doing too many things.
- Synchronous vs asynchronous communication. Use synchronous calls (HTTP, gRPC) for queries where the caller needs an immediate response. Use asynchronous messaging for commands where eventual consistency is acceptable. Asynchronous-first reduces coupling and improves resilience.
- Embrace eventual consistency. Cross-service data is eventually consistent by default in a microservices architecture. Design UIs and business logic that tolerate brief inconsistency — or use synchronous calls with circuit breakers where immediate consistency is critical.
- Invest in observability before splitting. Distributed systems are harder to debug than monoliths. Set up distributed tracing, structured logging with correlation IDs, and service-level alerting before you have more than two services. Retrofitting observability is painful.