Microservices Architecture: Independent Services, Bounded Contexts & Distributed Systems
Microservices decompose a large application into small, independently deployable services, each owning a single business capability and its own database. This guide covers bounded contexts, inter-service communication, independent scaling, data consistency trade-offs, and when to choose microservices over a monolith โ with live animations.
Microservices is an architectural style in which a single application is built as a suite of small, independently deployable services, each running in its own process, organized around a distinct business capability, and communicating over well-defined APIs. Rather than one large codebase that is deployed as a unit, you get a constellation of focused services that can be developed, scaled, and released independently of one another.
The term gained widespread attention after Netflix, Amazon, and Uber published how they broke their monolithic applications apart in the early 2010s. The driving insight was simple: a change to the checkout service should not require redeploying the recommendation engine. When different parts of the system evolve at different speeds and are owned by different teams, independent deployment boundaries dramatically reduce coordination overhead and deployment risk.
From Monolith to Microservices
A monolith packages all functionality โ user authentication, product catalog, orders, payments, notifications โ into one deployable artifact. Every change, no matter how small, goes through a single build and a single deployment pipeline. Early on this is fast; as the codebase grows, builds slow down, test suites balloon, and a bug in one module can crash the whole application. Microservices address this by splitting the monolith along business capability lines, each piece becoming its own service with its own repository, build pipeline, and deployment lifecycle.
Bounded Contexts: The Core Organising Principle
The concept of a bounded context comes from Domain-Driven Design (DDD). It defines a clear boundary within which a particular domain model applies and a single team has ownership. Inside the boundary, the team uses their own terminology, database schema, and internal APIs freely. Across boundaries, services communicate through versioned, explicitly-defined contracts. A good microservice is essentially a bounded context wrapped in a network API โ each service models one slice of the business domain end-to-end: its own logic, its own data, and its own deployment pipeline.
Choosing the right granularity is the hardest part of microservices design. Too coarse and you end up with a distributed monolith โ services that must be deployed together because of tight data coupling. Too fine and you accumulate hundreds of nano-services where every feature spans a dozen network hops. A practical heuristic: a service should be small enough for one team to own completely, yet large enough to be meaningfully deployed and operated on its own. Teams at Amazon famously use the two-pizza rule โ if a team needs more than two pizzas to feed, it is too large, and so is the service it owns.
Database-per-Service Pattern
One of the most important โ and most debated โ rules in microservices is database-per-service: each service owns and exclusively manages its own data store, and no other service is allowed to query that database directly. This enforces true loose coupling: the Orders service can migrate from PostgreSQL to DynamoDB without any other service caring, because all data access goes through the Orders service API. The cost is that joins across service boundaries become application-level joins, and maintaining consistency across services requires deliberate patterns like the Saga or Outbox.
Not every service needs the same type of database. The polyglot persistence approach lets each service pick the storage engine that fits its data model: relational PostgreSQL for the Order service (complex queries, ACID transactions), Redis for the Session service (key-value, low latency), Elasticsearch for the Search service (full-text), and Cassandra for the Metrics service (wide-column, high write throughput). Because each database is hidden behind a service API, the rest of the system never needs to know.
Inter-Service Communication
Services need to talk to each other, and the choice of communication style fundamentally shapes system behaviour. There are two broad categories: synchronous (the caller waits for a response) and asynchronous (the caller publishes an event and moves on).
Synchronous REST over HTTP is the most familiar approach โ services expose JSON APIs and call each other over HTTP. It is easy to understand and debug but creates temporal coupling: if the downstream service is slow or down, the caller blocks or fails. gRPC (Google Remote Procedure Call) uses Protocol Buffers over HTTP/2 for strongly-typed, binary-serialized calls that are two to ten times faster than JSON REST at scale. Use gRPC for high-throughput internal service-to-service calls where you control both sides.
Asynchronous event streaming via a message broker (Kafka, RabbitMQ, AWS SQS/SNS) decouples producers from consumers entirely. The Order service publishes an order.placed event; the Inventory service, Payment service, and Notification service each consume it independently. If Notification is down, the event queues up and is processed when it recovers โ no data is lost and the Order service never knew anything was wrong. The trade-off is eventual consistency and increased operational complexity from the broker.
| Style | Protocol | Best For | Trade-offs |
|---|---|---|---|
| REST | HTTP/1.1 + JSON | Public APIs, browser clients, simple integration | Verbose serialization, no built-in schema |
| gRPC | HTTP/2 + Protobuf | High-throughput internal calls, streaming | Requires code generation, harder to debug in browser |
| Message queue (push) | AMQP / RabbitMQ | Task queues, fan-out to one consumer group | At-most-once or at-least-once delivery guarantees vary |
| Event streaming (Kafka) | TCP custom protocol | Event sourcing, audit logs, replay, fan-out to many | Higher operational complexity, eventual consistency |
Scaling and Deployment Independence
The headline benefit of microservices is independent scaling: if your Payment service is CPU-bound during a flash sale, you scale it from 2 pods to 20 without touching the Recommendation service, the User service, or any other part of the system. With a monolith you must scale the entire application even if only 10% of it is the bottleneck. Kubernetes makes this natural โ each microservice becomes a Deployment with its own HorizontalPodAutoscaler, resource limits, and health probes.
Independent deployment is equally important. Teams can merge and ship code dozens of times a day to their own service without waiting for a monolith release train. Feature flags, canary deployments, and rolling updates become per-service decisions. The blast radius of a bad deployment is limited to one service, and rollback is fast because only that service's container image changes.
Request Fan-out: How a User Request Crosses Services
A single user-facing action โ placing an order โ might touch five services before a response reaches the client. An API Gateway sits at the edge, authenticates the request, rate-limits it, and routes it to the appropriate internal service. The Order service then calls the Inventory service synchronously to reserve stock, and publishes an order.placed event to a message broker. The Payment service and Notification service consume that event asynchronously, processing the charge and sending a confirmation email without blocking the original HTTP response. The user sees success within milliseconds; the email arrives a moment later.
The Costs: Distributed Systems Complexity
Microservices trade in-process simplicity for network complexity. Every service call is now a network call that can fail, time out, or return stale data. You must design for partial failure: implement retries with exponential back-off, circuit breakers (Hystrix, Resilience4j) to stop cascading failures, and timeouts on every outbound call. Distributed tracing (Jaeger, Zipkin, OpenTelemetry) becomes essential because a slow request now spans a call graph across a dozen services โ you cannot debug it with a single log file.
Data consistency is the deepest challenge. With a monolith and a single database you get ACID transactions for free. With microservices you must choose between the Saga pattern (a sequence of local transactions coordinated by events or a saga orchestrator, with compensating transactions for rollback) and eventual consistency. Neither is as simple as a database transaction. The Outbox pattern guarantees that a service's database write and its event publication to Kafka happen atomically by writing events to an outbox table in the same local transaction and publishing them asynchronously.
Microservices vs Monolith: When to Use Each
Microservices are not universally better than monoliths โ they are a trade-off that pays off under specific conditions. Martin Fowler's Microservice Premium describes the overhead: service mesh, distributed tracing, API versioning, container orchestration, and data consistency patterns all add complexity that a monolith simply doesn't have. For small teams or early-stage products, a well-structured modular monolith delivers most of the organisational benefits (clear module boundaries, independent testing) without the distributed-systems tax.
| Dimension | Monolith | Microservices |
|---|---|---|
| Deployment unit | Single artifact (JAR, WAR, binary) | Many independently deployed services |
| Scaling | Scale entire application together | Scale individual services independently |
| Data consistency | ACID transactions across modules | Eventual consistency; Saga / Outbox patterns |
| Development speed (small team) | Fast โ no network, no versioning | Slower โ API contracts, service boilerplate |
| Development speed (large org) | Slow โ big codebase, merge conflicts | Fast โ teams work independently |
| Failure blast radius | Bug can crash the whole app | Failures isolated to one service |
| Operational complexity | Low โ one process to monitor | High โ service mesh, tracing, many deployments |
| Best starting point | New products, small teams | Large orgs, many teams, high scale |
Cross-Cutting Concerns: Service Mesh and API Gateway
As the number of services grows, cross-cutting concerns โ mutual TLS between services, load balancing, retries, circuit breaking, telemetry โ would need to be implemented in every service. A service mesh (Istio, Linkerd) solves this by injecting a sidecar proxy (Envoy) alongside every service container. The sidecar handles all inter-service networking transparently, so application code stays clean. The mesh control plane gives operators a single place to configure traffic policies, observe latency, and enforce mTLS without changing any service code.
The API Gateway (Kong, AWS API Gateway, Nginx) is the single entry point for external clients. It handles authentication and authorisation (JWT validation, OAuth2), rate limiting, SSL termination, request routing, and protocol translation (REST to gRPC). This keeps these concerns out of individual services and gives the team a single place to audit and enforce API policies. The Backend for Frontend (BFF) pattern extends this: each client type (mobile, web, partner API) gets its own gateway that aggregates and shapes responses to match its specific needs.
Frequently Asked Questions
How small should a microservice be?
A microservice should map to a single bounded context โ one cohesive business capability owned end-to-end by one small team. In practice that might mean 1,000โ10,000 lines of code, a handful of REST or gRPC endpoints, and one or two database tables. If your service needs to import business logic from another service's codebase, it is probably too small. If a single team cannot understand the whole service in an hour, it is probably too large. Avoid the temptation of nano-services: splitting a User service into a Name service and an Email service creates more network hops than value.
How do you handle transactions that span multiple services?
You use the Saga pattern. A saga is a sequence of local transactions โ each service performs its own ACID transaction and publishes an event. If a step fails, compensating transactions (the saga's undo log) reverse the earlier steps. There are two flavours: choreography (services react to events with no central coordinator, simple but hard to visualise) and orchestration (a saga orchestrator service tells each participant what to do, easier to reason about but a single point of failure). For at-most-once event delivery guarantees, combine the Saga with the Outbox pattern โ write both your domain change and the outgoing event in a single local transaction to prevent silent data loss.
Should I start with microservices or migrate from a monolith?
Start with a modular monolith unless you already have multiple large teams that are blocked by deployment coupling. Well-defined module boundaries inside a monolith can be extracted into services later when the organisation and traffic justify it โ this is the Strangler Fig approach: new functionality is built as a service, and old monolith modules are progressively replaced service by service. Jumping straight to microservices before your domain model is stable leads to the worst outcome: a distributed monolith where services are coupled by data and must be deployed together anyway, with all the network overhead and none of the independence.
Microservices give every team a deployment boundary they own. The real engineering challenge is keeping those boundaries clean โ in your data model, your APIs, and your on-call rotation.
โ alokknight Engineering
