Skip to content

Microservices

Microservices is an architecture where the application is split into small, independently deployable services, each owning its data and business logic. Key patterns: API Gateway (single entry point), Service Discovery (Eureka), Circuit Breaker (fault tolerance), Saga (distributed transactions), CQRS (separate read/write models). Trade-offs vs monolith: more operational complexity, but better scalability and team independence.


Monolith vs Microservices

A monolith deploys as one unit — simple but hard to scale independently. Microservices split into independent services — each deployable, scalable, and owned by a small team. Trade-offs: microservices add network latency, distributed data challenges, and operational complexity. Start with a monolith, extract microservices as needed.

Deep Dive: Comparison
Aspect Monolith Microservices
Deployment One unit Independent services
Scaling Scale entire app Scale individual services
Technology One stack Polyglot
Team structure One team Small, autonomous teams
Complexity Code complexity Operational complexity
Data Shared database Database per service
Latency In-process calls Network calls
Testing Easier (one process) Harder (distributed)

API Gateway & Service Discovery

API Gateway — single entry point for all clients. Handles routing, authentication, rate limiting, load balancing, and SSL termination. Tools: Spring Cloud Gateway, Kong, AWS API Gateway. Service Discovery — services register themselves; others discover via registry instead of hardcoded URLs. Tools: Eureka, Consul, Kubernetes Service. Load Balancer — distributes traffic across instances (client-side with @LoadBalanced or server-side with Nginx/ALB).

Deep Dive: Architecture
Client → API Gateway → Load Balancer → Service Instance 1
                                     → Service Instance 2
                                     → Service Instance 3
          Service Registry (Eureka)

API Gateway responsibilities: - Request routing (path → service) - Authentication / Authorization - Rate limiting - Load balancing (lb://service-name) - Circuit breaking (fallback routes) - Request/response transformation

Service Discovery flow: 1. Service starts → registers with Eureka 2. Other services query Eureka for available instances 3. Client-side load balancing picks an instance 4. Services send heartbeats; Eureka removes dead instances


Resilience Patterns

Circuit Breaker — prevents cascading failures (CLOSED → OPEN → HALF_OPEN). Retry — retry with exponential backoff. Rate Limiter — limit requests per time window. Bulkhead — isolate resources so one failing service doesn't exhaust all threads. Time Limiter — set timeout for slow calls. Tools: Resilience4j, Spring Cloud CircuitBreaker.

Deep Dive: Circuit Breaker States & Config
CLOSED → (failures reach threshold) → OPEN
  ↑                                      │
  │         (wait duration)              ↓
  └──── CLOSED ←── (success) ←── HALF_OPEN
@CircuitBreaker(name = "userService", fallbackMethod = "fallback")
public User getUser(Long userId) {
    return restTemplate.getForObject("http://user-service/api/users/" + userId, User.class);
}

private User fallback(Long userId, Throwable ex) {
    return cachedUserService.getCachedUser(userId);  // Graceful degradation
}
Pattern Purpose Tool
Circuit Breaker Stop calls to failing service Resilience4j
Retry Retry with backoff Resilience4j
Rate Limiter Limit request rate Resilience4j / Bucket4j
Bulkhead Isolate thread pools Resilience4j
Time Limiter Timeout slow calls Resilience4j

Saga Pattern (Distributed Transactions)

In microservices, there's no single database transaction spanning services. The Saga pattern breaks a transaction into a sequence of local transactions — each publishes an event or calls the next step. If any step fails, compensating transactions undo previous steps. Two approaches: Choreography (event-driven, decentralized) and Orchestration (central coordinator).

Deep Dive: Choreography vs Orchestration
Order Service → Payment Service → Inventory Service
     │                                    │
     └── If any fails → compensating transactions (rollback)

Choreography — each service listens for events and reacts:

OrderService publishes OrderCreated
  → PaymentService listens, charges, publishes PaymentCompleted
    → InventoryService listens, deducts stock
✅ Loose coupling, simple
❌ Hard to track flow, debug

Orchestration — central Saga orchestrator controls flow:

SagaOrchestrator:
  1. Call OrderService.create()
  2. Call PaymentService.charge()
  3. Call InventoryService.deduct()
  On failure at step 3:
    → PaymentService.refund()
    → OrderService.cancel()
✅ Clear flow, easier to debug
❌ Central point of failure


CQRS (Command Query Responsibility Segregation)

CQRS separates the write model (commands — normalized, consistent) from the read model (queries — denormalized, optimized for reads). Useful when read and write patterns are very different (e.g., complex writes but high-volume simple reads). Often paired with Event Sourcing where state is stored as a sequence of events.

Deep Dive: Architecture
Write Side:            Read Side:
┌──────────┐          ┌──────────────┐
│ Command  │──event──→│ Event Handler│
│ Service  │          │ (projector)  │
│          │          └──────┬───────┘
│ (normalized DB)           │
└──────────┘          ┌─────↓───────┐
                      │ Query Model  │
                      │ (denormalized│
                      │  view DB)    │
                      └──────────────┘

When to use CQRS: - Read and write loads are very different - Complex domain logic on writes - Need different data models for reads vs writes - High read throughput (denormalized views)

When NOT to use: Simple CRUD applications — adds unnecessary complexity.


Inter-Service Communication

Synchronous — REST (simple, ubiquitous) or gRPC (fast, typed, streaming). Request-reply, caller blocks. Asynchronous — message queues (Kafka, RabbitMQ, SQS) for event-driven communication. Caller doesn't block, services are decoupled. Use sync for real-time queries, async for background processing, event-driven flows, and fault tolerance.

Deep Dive: Sync vs Async Comparison
Synchronous Asynchronous
Coupling Tighter Looser
Latency Request blocks Non-blocking
Failure handling Cascading Isolated
Complexity Simpler More complex
Use case Real-time queries Background processing

Messaging tools: - Kafka — high-throughput, log-based, replay, ordering - RabbitMQ — flexible routing, dead-letter queues - AWS SQS/SNS — managed, SQS for queues, SNS for pub-sub


Database Per Service

Each microservice owns its database — no shared databases, no cross-service JOINs. This enforces loose coupling but creates challenges: no distributed JOINs (use API composition), distributed transactions (use Saga), and data duplication (accept eventual consistency).

Deep Dive: Example
User Service    → PostgreSQL (users)
Order Service   → MongoDB (orders)
Payment Service → PostgreSQL (payments)
Search Service  → Elasticsearch (search index)

Challenges & solutions: - No cross-service JOINs → API composition or event-driven sync - Distributed transactions → Saga pattern - Data duplication → eventual consistency (acceptable in most cases)


Observability (Logging, Metrics, Tracing)

Three pillars: Logging (what happened — structured JSON logs, ELK/Kibana, Loki), Metrics (how it's performing — Prometheus + Grafana, Datadog), Distributed Tracing (request flow across services — Zipkin, Jaeger, Micrometer). Every request gets a trace ID propagated across all services.

Deep Dive: Tools & Examples

1. Logging — structured, centralized:

{"timestamp": "...", "service": "order", "traceId": "abc", "message": "Order created"}
Tools: ELK Stack (Elasticsearch, Logstash, Kibana), Loki

2. Metrics — request rates, latency, error rates:

request_duration_seconds{service="order"} 0.25
error_rate{service="order"} 0.01
Tools: Prometheus + Grafana, Datadog

3. Distributed Tracing — follow a request across services:

Request → Gateway (span1) → Order Service (span2) → Payment Service (span3)
Tools: Jaeger, Zipkin, Micrometer Tracing


Common Interview Questions

Common Interview Questions
  • What are microservices? How are they different from a monolith?
  • When should you use microservices vs a monolith?
  • What is the Saga pattern? Choreography vs orchestration?
  • What is CQRS? When would you use it?
  • What is a circuit breaker? Explain its states.
  • What resilience patterns do you know? (retry, bulkhead, rate limiter)
  • How do microservices communicate? Sync vs async?
  • What is the database-per-service pattern?
  • How do you handle distributed transactions?
  • What is observability? What are the three pillars?
  • What is event sourcing?
  • How do you handle cascading failures?