Microservices Architecture
Microservices decompose a monolithic Java application into independently deployable services aligned to business capabilities — each with its own codebase, database, and release…
Introduction
Microservices decompose a monolithic Java application into independently deployable services aligned to business capabilities — each with its own codebase, database, and release cadence. Netflix, Amazon, and Uber run hundreds of JVM microservices behind API gateways, service discovery, and event buses.
This lesson covers the four pillars every Java staff engineer must defend in architecture reviews: decomposition strategy (bounded contexts, strangler fig), database per service (data ownership, eventual consistency), API Gateway (routing, auth, rate limiting), and Service Discovery (Eureka, Consul, K8s DNS). Spring Boot + Spring Cloud is the default Java stack — but the patterns transcend any framework.
Business problem
Monolith pain at scale — why enterprises decompose:
- Deploy coupling: One-line change in notifications requires full 2M-line monolith redeploy — 45-minute pipeline, all teams blocked.
- Scaling mismatch: Payment service needs 50 pods; catalog needs 5 — monolith scales everything together, wasting 90% of compute.
- Technology lock-in: Single stack cannot adopt Kafka for one domain and Postgres for another without risky shared-module refactors.
- Blast radius: Memory leak in PDF export takes down checkout — no bulkheads between domains.
- Team autonomy: 200 engineers on one repo — merge conflicts, ownership disputes, slow reviews.
Why this topic exists
Microservices exist to optimize for organizational scale and independent evolution — not because "Netflix does it":
- Conway's Law: System architecture mirrors communication structure — align services to team boundaries.
- Database per service: Each service owns its schema — no shared tables, no cross-service JOINs in production.
- API Gateway: Single entry point for clients — hides internal topology, centralizes cross-cutting concerns.
- Service Discovery: Dynamic IPs in Kubernetes — clients resolve
payment-servicewithout hardcoded hosts. - Rejected shortcut: Distributed monolith — separate repos but shared database; worse than monolith.
Core concepts
Core microservices concepts for Java engineers:
- Decomposition: Split by subdomain (DDD bounded context), not by technical layer (all controllers in one service).
- Database per service: Order service owns
orderstable; Inventory ownsstock— sync via events or API. - API Gateway: Spring Cloud Gateway / Kong — route
/api/orders→ order-service, JWT validation, circuit breaker. - Service Discovery: Eureka (Netflix), Consul (HashiCorp), or Kubernetes Service DNS — register on startup, health-check deregister.
- Synchronous vs async: REST/gRPC for query/command; Kafka for events — avoid synchronous chains > 3 hops.
- Saga pattern: Distributed transactions via choreography (events) or orchestration (coordinator) — no 2PC across services.
Internal architecture
Typical Java microservices topology:
┌─────────────────┐Mobile/Web ───────▶│ API Gateway │ JWT · rate limit · routing└────────┬────────┘│┌───────────────────┼───────────────────┐▼ ▼ ▼┌───────────┐ ┌───────────┐ ┌───────────┐│ Order │ │ Payment │ │ Inventory ││ Service │ │ Service │ │ Service ││ (Postgres)│ │ (Postgres)│ │ (MongoDB) │└─────┬─────┘ └─────┬─────┘ └─────┬─────┘│ │ │└──────────────────┼──────────────────┘▼┌───────────────┐│ Kafka │ OrderCreated · PaymentCompleted└───────────────┘Service Discovery (Eureka / K8s):payment-service.default.svc.cluster.local → 10.0.1.42, 10.0.1.43
Three architecture views — decomposition, data ownership, and gateway/discovery:
Code walkthrough
Spring Boot microservice skeleton — order service with discovery registration:
- @EnableDiscoveryClient: Registers instance on startup; deregisters on graceful shutdown.
- Kafka publish: Order service emits events — inventory/payment consume without synchronous coupling.
- @FeignClient: Logical name
payment-service— discovery resolves to healthy instances. - Dedicated datasource: No shared
orderstable with monolith — migration via strangler.
// OrderServiceApplication.java@SpringBootApplication@EnableDiscoveryClient // registers with Eureka / Consulpublic class OrderServiceApplication {public static void main(String[] args) {SpringApplication.run(OrderServiceApplication.class, args);}}// OrderController.java — owns order domain only@RestController@RequestMapping("/orders")public class OrderController {private final OrderRepository repo;private final KafkaTemplate<String, OrderEvent> kafka;@PostMappingpublic Order create(@RequestBody CreateOrderRequest req) {Order order = repo.save(Order.from(req));kafka.send("order.events", new OrderCreated(order.id(), order.total()));return order;}}// PaymentClient.java — calls payment-service via discovery (Feign)@FeignClient(name = "payment-service") // resolved via Eureka/K8spublic interface PaymentClient {@PostMapping("/payments")PaymentResponse charge(@RequestBody PaymentRequest req);}// application.ymlspring:application.name: order-servicedatasource.url: jdbc:postgresql://order-db:5432/orders # dedicated DBeureka.client.serviceUrl.defaultZone: http://eureka:8761/eureka/
Production example
Spring Cloud Gateway + Eureka production stack:
- lb://order-service: Spring Cloud LoadBalancer + discovery — no hardcoded URLs.
- Rate limiter at gateway: Protect downstream from abuse — Redis-backed token bucket.
- Health probes: Unhealthy instances removed from discovery within seconds.
- K8s alternative: Skip Eureka — use Kubernetes Service + Spring Cloud K8s discovery.
# gateway application.ymlspring:cloud:gateway:routes:- id: ordersuri: lb://order-service # load-balanced via discoverypredicates:- Path=/api/orders/**filters:- StripPrefix=1- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 100- id: paymentsuri: lb://payment-servicepredicates:- Path=/api/payments/**# Eureka server (or use Kubernetes native discovery in K8s)eureka:server:enable-self-preservation: false # faster eviction in dev; tune for prod# Order service — health for discoverymanagement:endpoints.web.exposure.include: health,infoendpoint.health.probes.enabled: true # K8s liveness/readiness
Enterprise case study
Amazon — service-oriented architecture origin: Amazon mandated that all teams expose data only via service APIs — no direct database access across teams. This forced database-per-service before the term existed. Two-pizza teams own order pipeline, catalog, and fulfillment services independently. Lesson for Java teams: decomposition succeeds when data ownership is enforced, not when repos are merely split.
- Before: Shared Oracle schema — schema migration required 15-team approval.
- Decision: API-only access; each team owns PostgreSQL instance per domain.
- After: Independent deploy cadence; Black Friday scale per service.
- Trade-off: Eventual consistency — sagas replace ACID cross-service transactions.
Performance considerations
Microservices performance realities:
- Network overhead: Every service call adds 1–5ms LAN latency — minimize chatty synchronous chains.
- Connection pools: Each service × each downstream = pool explosion — use HTTP/2, connection reuse, bulk APIs.
- Caching at gateway: Cache GET responses for catalog — reduce fan-out to 10 services per page load.
- Async where possible: OrderCreated event → inventory reserves stock — user doesn't wait for inventory HTTP round-trip.
- JVM per service: Right-size heap per service — payment 512MB, batch worker 2GB — not one-size-fits-all monolith heap.
Security considerations
Security in distributed Java services:
- Gateway as security perimeter: JWT validation once at gateway — internal services trust mTLS or network policy.
- Service-to-service auth: OAuth2 client credentials, SPIFFE/SPIRE, or Istio mTLS — never "trust internal network."
- Secrets per service: Payment DB credentials only in payment-service K8s Secret — not shared ConfigMap.
- Rate limiting: Gateway + per-service limits — prevent lateral movement after compromise.
Scalability considerations
Scaling microservices independently:
- HPA per deployment: Order service scales on CPU; notification worker scales on Kafka lag.
- Stateless services: Session in Redis — any pod handles any request; scale horizontally freely.
- Database per service scaling: Read replicas for catalog; sharding for orders — tune per domain load.
- Discovery at scale: Eureka self-preservation vs fast eviction — tune for your failure tolerance.
Production challenges
Real microservices migration pain:
- Distributed debugging: Request spans 6 services — need distributed tracing (OpenTelemetry) from day one.
- Data duplication: Product name cached in order service — accept duplication or sync via events.
- Testing complexity: Contract tests (Pact), testcontainers for integration — unit tests alone insufficient.
- Operational overhead: 50 services = 50 deploy pipelines, 50 dashboards — invest in platform team.
- Partial failures: Payment down — order service must degrade gracefully (queue, retry, compensating saga).
Common mistakes
- Decomposing by technical layer (API service, DB service) instead of business domain — recreates distributed monolith.
- Shared database across microservices — coupling worse than monolith; migrations block all teams.
- Synchronous call chains Order → Inventory → Payment → Notification — latency multiplies; fragile under load.
- Skipping API Gateway — mobile clients call 12 service URLs directly; auth duplicated everywhere.
- Big-bang rewrite instead of strangler fig — 18-month freeze, competitor ships features.
Debugging guide
Debug microservices production issues:
- Trace ID propagation:
X-Trace-Idor W3C traceparent from gateway through all Feign calls. - Eureka dashboard: Verify instance registered, health UP — stale registry causes 503 to ghost instances.
- Gateway route logs:
logging.level.org.springframework.cloud.gateway=DEBUG— see which route matched. - Feign errors: Enable full request/response logging in staging — 404 often means wrong service name in discovery.
# Verify service registration (Eureka REST)curl http://eureka:8761/eureka/apps/ORDER-SERVICE# K8s service endpointskubectl get endpoints order-service -n production# Test gateway routecurl -H "Authorization: Bearer $JWT" https://api.example.com/api/orders/123
Best practices
- Decompose by bounded context — order, payment, catalog — align to team ownership.
- Enforce database per service — no cross-service SQL; use events or API for data sync.
- Deploy API Gateway for all external traffic — auth, rate limit, routing in one place.
- Use service discovery (K8s DNS or Eureka) — never hardcode service IPs in Java code.
- Prefer async events (Kafka) for cross-domain workflows — sagas over distributed transactions.
- Migrate with strangler fig — extract one capability at a time; monolith shrinks gradually.
- Invest in observability (OpenTelemetry) before service count exceeds 10.
Anti-patterns
- Distributed monolith — separate Git repos but shared database and synchronous-only coupling.
- Nano-services — one endpoint per service; operational overhead exceeds benefit.
- Smart endpoints, dumb pipes reversed — business logic in gateway instead of domain services.
- Chatty REST — 20 HTTP calls per user request; use BFF aggregation or GraphQL at gateway.
- Shared library with domain entities — Order class in common.jar couples all services to one model.
Staff engineer notes
- Staff engineers ask "do we need microservices?" first — monolith + modular monolith wins for teams under 10.
- Database per service is non-negotiable — without it you have a distributed monolith with extra network hops.
- API Gateway is the product's front door — treat gateway config changes like API contract changes (review, test, canary).
- Service discovery is infrastructure — on Kubernetes, prefer native K8s Services over running Eureka unless multi-cluster.
- Measure decomposition success by deploy frequency and blast radius — not service count on architecture diagram.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is microservices architecture?
BeginnerModel answer
Decomposition of application into small, independently deployable services aligned to business capabilities.
Each service owns its data, has separate codebase and deploy pipeline.
Communicate via APIs and events.
Follow-up probe
vs monolith?
2What is database per service?
BeginnerModel answer
- Each microservice has private database schema
- only that service reads/writes. No cross-service JOINs. Data sync via API calls or event streaming. Enables independent schema evolution.
Follow-up probe
How handle transactions?
3What is an API Gateway?
BeginnerModel answer
Single entry point for clients.
Routes requests to internal services, handles cross-cutting concerns: authentication, rate limiting, SSL termination, request aggregation.
Examples: Spring Cloud Gateway, Kong, AWS API Gateway.
Follow-up probe
Gateway vs load balancer?
4What is service discovery?
BeginnerModel answer
Mechanism for services to find each other dynamically without hardcoded IPs.
Services register on startup; clients query registry for healthy instances.
Eureka, Consul, Kubernetes DNS.
Follow-up probe
Client-side vs server-side?
5How do you decompose a monolith?
BeginnerModel answer
- Identify bounded contexts (DDD). Use strangler fig
- route new traffic to extracted service via gateway. Start with low-risk read-only domains. Database per extracted service. Event-driven sync for remaining coupling.
Follow-up probe
First service to extract?
Intermediate
6Explain the strangler fig pattern.
IntermediateModel answer
Incrementally replace monolith by routing slices of functionality to new services while legacy handles rest.
Proxy/gateway decides routing.
Over time monolith shrinks until decommissioned.
No big-bang rewrite.
Follow-up probe
Routing mechanism?
7How handle distributed transactions?
IntermediateModel answer
Avoid 2PC across services.
Use saga pattern: choreography (each service listens/emits events) or orchestration (central coordinator).
Compensating transactions on failure.
Accept eventual consistency.
Follow-up probe
Saga vs 2PC?
8API Gateway vs BFF (Backend for Frontend)?
IntermediateModel answer
- Gateway: generic routing, auth, rate limit for all clients. BFF: tailored API per client type (mobile vs web)
- aggregates multiple services into one response. Often BFF sits behind or alongside gateway.
Follow-up probe
One BFF or many?
9Eureka vs Kubernetes service discovery?
IntermediateModel answer
Eureka: Netflix OSS registry, client-side load balancing with Ribbon/LoadBalancer.
local), kube-proxy load balances.
On K8s, prefer native discovery; Eureka for multi-cluster or non-K8s workloads.
Follow-up probe
Self-preservation mode?
10What is a distributed monolith?
IntermediateModel answer
- Anti-pattern: services deployed separately but tightly coupled via shared database, synchronous calls, shared libraries. Worst of both worlds
- network overhead without autonomy. Fix: database per service, async events.
Follow-up probe
How detect?
Advanced
11Design e-commerce checkout as microservices.
AdvancedModel answer
Services: Catalog (read-heavy), Cart (Redis), Order (Postgres), Payment (PCI scope), Inventory (events).
Gateway routes /api/*.
Kafka: OrderCreated → inventory reserve, payment charge.
Saga: payment fail → cancel order.
Discovery: K8s Services.
DB per service.
Follow-up probe
Sync vs async boundaries?
12When NOT to use microservices?
AdvancedModel answer
- Small team (<10), unclear domain boundaries, low traffic, startup validating product. Modular monolith with clear module boundaries scales to millions of users (Shopify early). Microservices add operational complexity
- justify with team/deploy scale.
Follow-up probe
Modular monolith?
13How migrate 500K LOC Spring monolith?
AdvancedModel answer
Phase 1: modular monolith + gateway in front.
Phase 2: extract catalog (read-only, CDN-friendly).
Phase 3: cart with Redis.
Phase 4: order + payment saga last (highest risk).
Parallel run: dual-write or event sync.
Contract tests.
12–24 month timeline realistic.
Follow-up probe
Biggest failure mode?
14Service mesh vs API Gateway?
AdvancedModel answer
- Gateway: north-south traffic (client → cluster). Service mesh (Istio/Linkerd): east-west (service → service)
- mTLS, retries, observability at sidecar. Complementary: gateway for external; mesh for internal policy.
Follow-up probe
Need both?
15How ensure idempotency across services?
AdvancedModel answer
Idempotency keys on payment/order APIs.
Event consumers track processed message IDs (dedup table).
Kafka exactly-once or at-least-once + idempotent handlers.
Never assume single delivery.
Follow-up probe
Idempotency key storage?
Hands-on exercise
Lab: Microservices concepts — extend playground:
- Run demo — observe sealed event types and virtual thread executor pattern for async publish.
- Add InventoryReserved event to sealed hierarchy — update switch exhaustively.
- Write FeignClient interface stub for payment-service with @PostMapping.
- Sketch gateway route YAML mapping /api/orders to order-service.
- Bonus: list 3 bounded contexts you'd extract first from a banking monolith and why.
JavaMicroservices Architecture
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Microservices vs monolith: Microservices win team scale and independent deploy; monolith wins simplicity and latency.
- Sync REST vs async Kafka: REST wins immediate consistency queries; Kafka wins decoupling and peak load absorption.
- Eureka vs K8s DNS: Eureka wins non-K8s and client-side LB; K8s wins operational simplicity on cluster.
- Choreography vs orchestration saga: Choreography wins loose coupling; orchestration wins visibility and debugging.
Summary
Microservices are an organizational and data-ownership strategy — not a technology checkbox. Master decomposition, database per service, API Gateway, and Service Discovery before debating Spring Cloud vs Quarkus. Next: pair this with Kafka for event-driven sagas and OpenTelemetry for cross-service debugging.
Key takeaways
- Decompose by bounded context — database per service is the ownership boundary.
- API Gateway: single client entry — auth, routing, rate limiting.
- Service Discovery: dynamic resolution — Eureka or Kubernetes DNS.
- Strangler fig migration — no big-bang rewrite.
- Sagas and events for cross-service workflows — not distributed 2PC.