Microservices Architecture
Microservices structure a system as a set of small, independently deployable services that each own their data and behavior. The payoff is team autonomy and independent scaling; the cost is distributed-systems complexity.
That trade-off is the whole story. Microservices solve an organizational and scaling problem, not a coding one — and a modular monolith is usually the right place to start.
TL;DR
- Start with a modular monolith unless you have a clear reason not to.
- If you go microservices: strong boundaries, one owner per dataset, full observability.
- Prefer async communication for decoupling — accept eventual consistency.
- Use sagas and the outbox pattern instead of distributed transactions.
Quick Example
The defining rule in code — a service never reaches into another's database; it calls an API or reacts to an event:
Monolith vs Microservices
Costs that arrive with services: network latency, partial failures, data consistency, and operational overhead. Don't pay them without a reason.
Service Boundaries
Good boundaries follow business capabilities (billing, identity, content, notifications), not technical layers (controller/service/DAO — that just creates chatty calls). A service should own a cohesive domain and evolve without constant cross-team coordination.
Data Ownership
Each service owns its own database/schema. Others read or change that data only through its API or events — never by querying its tables directly. A shared database silently recouples everything and is the most common way microservices fail.
Communication
Synchronous (REST/gRPC)
Simple mental model, but introduces runtime coupling and cascading failures. See REST API Design.
Asynchronous (events)
Publish events; consumers react. Decouples producers from consumers and enables fan-out, at the cost of eventual consistency. See Message Queues.
Reliability Patterns
- Timeouts + retries — always set timeouts; retry only safe operations with exponential backoff and jitter.
- Circuit breakers — stop hammering a failing dependency.
- Bulkheads — isolate pools/threads so one slow dependency can't sink the whole service.
- Idempotency — make retried requests and redelivered events safe.
Distributed Transactions
ACID transactions across services don't scale. Use:
- Saga — break a workflow into steps, each with a compensating action that undoes it on failure.
- Outbox pattern — write the domain change and an outbox record in one DB transaction, then publish from the outbox, so you never "update the DB but lose the message."
Common Mistakes
Sharing a database between services
Splitting too early
FAQ
Should I start with microservices?
Usually no. Begin with a well-structured (modular) monolith — it's far simpler to build, deploy, and keep consistent. Extract services once you have a concrete driver: independent scaling, team autonomy, or failure isolation.
How big should a service be?
Big enough to own a complete business capability and evolve independently, small enough for one team to own. "One service per developer" or per database table are anti-patterns that create chatty, brittle systems.
How do I handle a transaction that spans services?
You don't use a distributed ACID transaction. Model it as a saga with compensating actions, and use the outbox pattern to publish events reliably alongside your database writes.
Sync or async communication?
Async (events) where you can — it decouples services and contains failures. Use sync (REST/gRPC) for request/response interactions that genuinely need an immediate answer, with timeouts and circuit breakers.
Related Topics
- Event-Driven Architecture — The async backbone
- Message Queues — Brokers, delivery, and the outbox
- REST API Design — Synchronous service APIs
- Logging — Correlating across services
- API Security — Securing service-to-service calls
- Monolith vs Microservices — Whether to split at all