Monolith vs. Microservices
Monolith
- Single codebase, single deployment unit.
- All modules share the same process and database.
- Simple to develop, test, and deploy initially.
- Harder to scale individual components.
- A bug in one module can crash the entire application.
- Long deployment cycles as the codebase grows.
Microservices
- Multiple codebases, independently deployable services.
- Each service has its own database (ideally).
- Complex to develop, test, and operate.
- Scale individual services based on their specific load.
- Failure in one service can be isolated from others.
- Small, frequent deployments per service.
When to Use Microservices
Microservices are not always the right choice. They introduce significant operational complexity. Use microservices when:
- Your team is large enough that a monolith causes merge conflicts and release bottlenecks.
- Different parts of the system have vastly different scaling requirements.
- You need independent deployment cycles for different features.
- You want to use different technology stacks for different services.
- You can invest in the operational infrastructure (CI/CD, monitoring, service mesh).
Service Boundaries
The hardest part of microservices is deciding where to draw the lines. Use Domain-Driven Design (DDD) to find natural boundaries:
- Bounded Contexts: Each service should correspond to a bounded context: a distinct area of the business with its own models and language.
- Single Responsibility: Each service should do one thing well.
- Data Ownership: Each service owns its data exclusively. No shared databases between services.
- Team Alignment: Service boundaries should align with team boundaries (Conway's Law).
Communication Patterns
Synchronous (Request-Response)
Services call each other via HTTP/REST or gRPC. Simple and intuitive, but creates temporal coupling: the caller is blocked until the callee responds.
Asynchronous (Event-Driven)
Services communicate through message queues or event streams. Decoupled in time: the producer does not wait for the consumer. Preferred for operations that do not need an immediate response.
Service Mesh
A dedicated infrastructure layer (e.g., Istio, Linkerd) that handles service-to-service communication, including discovery, load balancing, encryption, observability, and retry logic. The application code does not need to implement these concerns.
Service Discovery
In a dynamic environment where services scale up and down, how does Service A know where Service B is running?
- Client-side discovery: The client queries a service registry (e.g., Consul, Eureka) to find available instances and load balances requests itself.
- Server-side discovery: The client sends requests to a load balancer or API gateway, which queries the registry and routes the request.
- DNS-based: Services register DNS entries. The calling service resolves the DNS name. Kubernetes services work this way.
API Gateway
An API gateway is a single entry point for all client requests. It routes requests to the appropriate microservice and provides cross-cutting concerns:
- Request routing and composition
- Authentication and authorization
- Rate limiting and throttling
- SSL termination
- Response caching
- Request/response transformation
Data Management
Each microservice should own its database. This means:
- No direct database sharing between services. Other services access data through the owning service's API.
- Data consistency across services is eventual, not immediate.
- Distributed transactions are replaced by Sagas: a sequence of local transactions coordinated through events.
Saga Pattern
A saga is a sequence of transactions where each step has a compensating transaction in case of failure:
- Order Service creates an order (pending).
- Payment Service charges the customer.
- Inventory Service reserves the items.
- If any step fails, previous steps execute their compensating actions (refund payment, cancel order).
Challenges of Microservices
- Distributed system complexity: Network calls can fail, be slow, or return partial results.
- Data consistency: Maintaining consistency across services is harder than within a single database.
- Testing: Integration testing across services is complex. Contract testing helps.
- Debugging: A single user request may traverse many services. Distributed tracing is essential.
- Operational overhead: Each service needs its own CI/CD pipeline, monitoring, logging, and alerting.
- Deployment coordination: Breaking API changes require careful versioning and rollout.
Key Takeaways
- Start with a well-structured monolith. Extract microservices when complexity demands it.
- Define service boundaries using domain-driven design and bounded contexts.
- Each service owns its data. No shared databases.
- Prefer asynchronous communication for loose coupling; use synchronous only when immediate responses are required.
- Invest heavily in observability: distributed tracing, centralized logging, and service-level metrics.
- Use an API gateway to centralize cross-cutting concerns.
- Microservices trade development simplicity for operational complexity. Make sure the trade is worth it.