Microservices Architecture

A comprehensive guide to microservices architecture covering service decomposition, API gateways, service mesh, domain-driven design, communication patterns, and practical implementation strategies for building scalable distributed systems.

Design Patterns Domain-Driven Design Service Mesh Kubernetes Native
What is Microservices Architecture?

Microservices architecture is an approach to building software systems as a collection of small, independently deployable services. Each service runs in its own process, communicates via lightweight protocols (typically HTTP/REST or messaging), and is organized around business capabilities.

This architectural style enables organizations to scale development teams, deploy independently, and adopt different technologies for different services. However, it also introduces significant complexity in areas like service discovery, data consistency, and distributed tracing.

Client API Gateway Service A Service B Service C Database per Service
Key Principle: Microservices are not just about technical architecture—they're about organizational scalability. Each service should be small enough to be owned by a single team, allowing teams to move independently and deploy frequently.
Service Decomposition Strategies

Decomposing a monolithic application into microservices is one of the most challenging aspects of adopting this architecture. The goal is to create services that are cohesive, loosely coupled, and aligned with business capabilities.

Decompose by Business Capability

Domain-driven decomposition
Identify business capabilities (e.g., Order Management, Customer Management, Inventory) and create a service for each. This aligns services with business functions and ensures cohesive boundaries.
E-commerce, Enterprise applications

Decompose by Data Domain

Data-driven decomposition
Split based on data ownership and access patterns. Services own their data and expose it through APIs. This reduces coupling and enables independent scaling of data stores.
Data-intensive applications

Decompose by Subdomain

Strategic Domain-Driven Design
Identify bounded contexts and subdomains. Each subdomain becomes a service. This approach uses DDD concepts to find natural boundaries in the problem domain.
Complex business domains

Strangler Fig Pattern

Incremental migration
Gradually replace pieces of the monolith with microservices. Start with new features, then incrementally migrate existing functionality while routing traffic appropriately.
Legacy system modernization
Common Pitfall: Avoid creating too many microservices too early. Start with a few well-defined services and decompose further as you understand the domain better. Over-decomposition leads to distributed monolith and excessive complexity.
Domain-Driven Design (DDD) for Microservices

Domain-Driven Design provides a framework for modeling complex business domains and finding the right service boundaries. It's essential for successful microservices architecture.

Bounded Contexts

Explicit service boundaries
A bounded context is a logical boundary where a particular model is defined and applicable. Each microservice typically represents one bounded context with its own ubiquitous language.
Defining service scope

Ubiquitous Language

Shared team vocabulary
A common language used by both domain experts and developers. It ensures that the code reflects the business domain and reduces miscommunication between teams.
Team communication

Aggregates

Consistency boundaries
An aggregate is a cluster of domain objects treated as a single unit for data changes. Each aggregate has a root entity that enforces business rules and maintains consistency.
Transactional consistency

Domain Events

Event-driven communication
Explicit events representing significant business occurrences. They enable loose coupling between services and support eventual consistency patterns.
Event-driven architecture
// Domain Aggregates Example (Order Management) public class Order extends AggregateRoot { private OrderId id; private CustomerId customerId; private List<OrderItem> items; private OrderStatus status; private Money totalAmount; public void addItem(Product product, int quantity) { // Business rule: validate stock // Domain event: OrderItemAdded } public void complete() { if (items.isEmpty()) { throw new DomainException("Cannot complete empty order"); } this.status = OrderStatus.COMPLETED; registerEvent(new OrderCompletedEvent(this)); } } // Bounded Context: Order Management // Bounded Context: Inventory Management // Bounded Context: Customer Management
API Gateway Pattern

The API Gateway is a single entry point for clients that routes requests to appropriate microservices. It handles cross-cutting concerns like authentication, rate limiting, logging, and request aggregation.

Request Routing

Intelligent routing
Routes incoming requests to the appropriate microservice based on path, headers, or other criteria. Supports versioning and canary deployments.
Service discovery

Authentication & Authorization

Security enforcement
Centralizes authentication (JWT validation, OAuth) and authorization checks. Removes this burden from individual services.
Security

Request Aggregation

Compose responses
Combines responses from multiple services into a single response, reducing client-side complexity and network round trips.
Frontend applications

Rate Limiting & Circuit Breaking

Resilience
Protects services from overload with rate limiting. Implements circuit breakers to prevent cascading failures.
Resilience engineering
# Kong API Gateway Configuration _format_version: "3.0" services: - name: order-service url: http://order-service:8080 routes: - name: order-route paths: - /api/v1/orders plugins: - name: jwt - name: rate-limiting config: minute: 100 hour: 1000 - name: customer-service url: http://customer-service:8080 routes: - name: customer-route paths: - /api/v1/customers plugins: - name: jwt # Istio VirtualService (Kubernetes) apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: api-gateway spec: hosts: - api.example.com gateways: - api-gateway http: - match: - uri: prefix: /api/v1/orders route: - destination: host: order-service port: number: 8080 - match: - uri: prefix: /api/v1/customers route: - destination: host: customer-service port: number: 8080
Best Practice: Popular API Gateway implementations include Kong (open-source, enterprise), NGINX, Traefik, and Ambassador. In Kubernetes, you can also use the Istio Ingress Gateway or AWS API Gateway for cloud-native deployments.
Service Mesh: Advanced Service-to-Service Communication

A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It provides advanced networking capabilities including traffic management, security (mTLS), observability, and resilience patterns without requiring changes to application code.

mTLS Security

Zero-trust networking
Mutual TLS encryption between services. Each service has a certificate, and communication is automatically encrypted and authenticated.
Security & compliance

Traffic Management

Advanced routing
Canary deployments, A/B testing, and circuit breaking at the network level. Traffic can be split based on headers, weights, or other criteria.
Deployment strategies

Observability

Metrics, logs, traces
Automatic collection of metrics (latency, error rate, traffic), distributed tracing, and access logs for all service-to-service communication.
Monitoring & debugging

Policy Enforcement

Authorization & quota
Enforce authorization policies (who can talk to whom), rate limiting, and quota management at the network layer.
Security & governance
# Istio Service Mesh Configuration apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: order-service spec: host: order-service trafficPolicy: connectionPool: tcp: maxConnections: 100 loadBalancer: simple: ROUND_ROBIN tls: mode: ISTIO_MUTUAL --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: order-service spec: hosts: - order-service http: - match: - headers: canary: exact: "true" route: - destination: host: order-service subset: v2 weight: 50 - destination: host: order-service subset: v1 weight: 50 --- apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: order-service-auth spec: selector: matchLabels: app: order-service rules: - from: - source: namespaces: ["default", "production"] principals: ["cluster.local/ns/default/sa/order-sa"]
Consideration: Service meshes add complexity and resource overhead. They're most valuable in large-scale deployments with many services. For smaller setups, simpler patterns like client libraries or API gateways may be sufficient.
Service Communication Patterns

Microservices need to communicate with each other. Choosing the right communication pattern is critical for performance, reliability, and maintainability.

Synchronous HTTP/REST

Request-response
Simple, familiar, and widely adopted. Good for query operations but can lead to tight coupling and cascading failures.
Simple operations, queries

Asynchronous Messaging

Event-driven
Services communicate via message queues or event streams. Decouples services, improves resilience, and enables eventual consistency.
Events, long-running operations

Event Sourcing

Event store
State changes are stored as a sequence of events. Enables audit trails, replayability, and temporal queries. Often used with CQRS.
Audit, compliance, complex business

CQRS

Command Query Responsibility Segregation
Separates read and write models. Enables different optimization for queries and commands, and can use different data stores.
High-performance systems
# Asynchronous Messaging with Kafka # Producer (Order Service) @EventListener public void onOrderCreated(OrderCreatedEvent event) { kafkaTemplate.send("order-events", event); } # Consumer (Inventory Service) @KafkaListener(topics = "order-events") public void handleOrderCreated(OrderCreatedEvent event) { inventoryService.reserveStock(event.getOrderId(), event.getItems()); } # Event Sourcing Example @EventSourcingHandler public void handle(OrderCreatedEvent event) { this.id = event.getOrderId(); this.customerId = event.getCustomerId(); this.items = event.getItems(); this.status = OrderStatus.CREATED; } @EventHandler public void on(OrderCompletedEvent event) { // Update read model orderReadRepository.updateStatus(event.getOrderId(), event.getStatus()); }
Data Management in Microservices

Data management is one of the most challenging aspects of microservices. Each service should own its data, but this introduces complexity for distributed transactions and queries.

Database per Service

Data isolation
Each service has its own database (or schema). This ensures loose coupling and independent evolution but makes cross-service queries difficult.
Independent scaling

Saga Pattern

Distributed transactions
Manages distributed transactions as a sequence of local transactions, each with a compensating action for rollback. Coordinates multiple services to maintain consistency.
Complex business operations

API Composition

Query aggregation
Complex queries are handled by a service that aggregates data from multiple services. This pattern is simpler than CQRS for read-heavy workloads.
Read operations

Eventual Consistency

Eventual data consistency
Accept that data may be temporarily inconsistent between services. Use events to propagate changes and ensure consistency over time.
Distributed systems
# Saga Pattern Example (Orchestration) @Saga public class OrderSaga { @Step(compensation = "cancelOrder") public void createOrder(OrderCreateRequest request) { orderService.createOrder(request); } @Step(compensation = "rollbackInventory") public void reserveInventory(OrderCreateRequest request) { inventoryService.reserveStock(request.getOrderId(), request.getItems()); } @Step(compensation = "refundPayment") public void processPayment(OrderCreateRequest request) { paymentService.processPayment(request.getOrderId(), request.getTotal()); } @Step public void completeOrder(OrderCreateRequest request) { orderService.completeOrder(request.getOrderId()); } // Compensation methods public void cancelOrder(OrderCreateRequest request) { orderService.cancelOrder(request.getOrderId()); } public void rollbackInventory(OrderCreateRequest request) { inventoryService.releaseStock(request.getOrderId(), request.getItems()); } public void refundPayment(OrderCreateRequest request) { paymentService.refundPayment(request.getOrderId()); } }
Implementing Microservices on Kubernetes

Kubernetes is the ideal platform for running microservices. It provides service discovery, load balancing, scaling, and self-healing capabilities out of the box.

# Service Definition apiVersion: v1 kind: Service metadata: name: order-service spec: selector: app: order-service ports: - port: 8080 targetPort: 8080 --- # Deployment apiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: myregistry/order-service:latest ports: - containerPort: 8080 env: - name: DB_URL valueFrom: secretKeyRef: name: order-db-secret key: url resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 20 periodSeconds: 5 --- # HPA for auto-scaling apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: order-service-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: order-service minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80
Kubernetes Best Practices for Microservices:
  • Use namespaces for environment isolation (dev, staging, production)
  • Implement health checks (liveness and readiness probes)
  • Use resource requests and limits for predictable performance
  • Configure HPA for automatic scaling based on metrics
  • Use Service Mesh (Istio, Linkerd) for advanced traffic management
  • Implement PodDisruptionBudgets for high availability
Frequently Asked Questions
When should I use microservices vs. monolith?
Use microservices when you have a large team (multiple teams), need independent deployment, or have complex business domains that can be decomposed. Start with a monolith if you're small, have a simple domain, or are uncertain about service boundaries. Many successful organizations start with a monolith and decompose gradually.
What is the difference between an API Gateway and a Service Mesh?
An API Gateway handles external traffic (client-to-service) and provides cross-cutting concerns like authentication, rate limiting, and request aggregation. A Service Mesh handles internal traffic (service-to-service) and provides advanced networking, security (mTLS), and observability. Both can coexist in a system.
What is Domain-Driven Design and why is it important for microservices?
Domain-Driven Design provides a systematic approach to modeling complex business domains. It helps identify bounded contexts (service boundaries) and establishes a ubiquitous language shared by domain experts and developers. DDD is essential for designing microservices that truly reflect business capabilities.
How do you handle distributed transactions in microservices?
Use the Saga pattern. Sagas coordinate a sequence of local transactions across services, with compensating actions for rollback. Implement sagas using either orchestration (central coordinator) or choreography (event-based). Eventually consistency is preferred over distributed transactions (2PC).
What is the Strangler Fig pattern?
The Strangler Fig pattern is an incremental approach to modernizing legacy systems. Instead of replacing the entire system at once, you gradually replace pieces with new services. Traffic is routed to new services when ready, and the legacy system is eventually "strangled" and retired.
How do you handle data consistency across microservices?
Data consistency is typically handled using eventual consistency. Each service owns its data and exposes changes through events. Other services consume these events and update their state accordingly. For transactions, use the Saga pattern with compensating actions.
What are the challenges of microservices architecture?
Key challenges include: increased operational complexity (deployment, monitoring), network latency and reliability issues, data consistency across services, distributed tracing and debugging, inter-service communication patterns, team organization and coordination, and managing the distributed monolith anti-pattern.
Should every microservice have its own database?
Yes, for production deployments. Each service should own its data to maintain loose coupling and independent evolution. This allows each service to choose the right database technology and scale independently. However, it introduces challenges with cross-service queries and distributed transactions.
Previous: Cluster Setup Next: Multi-Cluster Architecture

Microservices architecture is a powerful approach for building scalable, resilient distributed systems. Focus on proper service boundaries, invest in observability, and embrace eventual consistency for successful implementation.