Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 25

    Microservices Architecture

    Microservices decompose a monolithic Java application into independently deployable services aligned to business capabilities — each with its own codebase, database, and release…

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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-service without 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 orders table; Inventory owns stock — 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:

    text
    ┌─────────────────┐
    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:

    Decomposition by bounded context
    Monolith
    Single deploy unit
    Strangler
    Route slice by slice
    Services
    Order · Pay · Catalog
    Events
    Kafka decoupling
    Extract by business capability — not by layer (all DAOs in one service).
    Database per service
    Order DB
    Owns orders schema
    Payment DB
    Owns payments schema
    No JOINs
    Cross-service via API/event
    Saga
    Eventual consistency
    Each service is the sole writer of its data — shared DB is an anti-pattern.
    Gateway + Service Discovery
    Client
    Single base URL
    Gateway
    Auth · route · limit
    Discovery
    Eureka / K8s DNS
    Instances
    N replicas each
    Clients never call service IPs directly — gateway + discovery handle topology.

    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 orders table with monolith — migration via strangler.
    java
    // OrderServiceApplication.java
    @SpringBootApplication
    @EnableDiscoveryClient // registers with Eureka / Consul
    public 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;
    @PostMapping
    public 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/K8s
    public interface PaymentClient {
    @PostMapping("/payments")
    PaymentResponse charge(@RequestBody PaymentRequest req);
    }
    // application.yml
    spring:
    application.name: order-service
    datasource.url: jdbc:postgresql://order-db:5432/orders # dedicated DB
    eureka.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.
    java
    # gateway application.yml
    spring:
    cloud:
    gateway:
    routes:
    - id: orders
    uri: lb://order-service # load-balanced via discovery
    predicates:
    - Path=/api/orders/**
    filters:
    - StripPrefix=1
    - name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 100
    - id: payments
    uri: lb://payment-service
    predicates:
    - 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 discovery
    management:
    endpoints.web.exposure.include: health,info
    endpoint.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-Id or 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.
    bash
    # Verify service registration (Eureka REST)
    curl http://eureka:8761/eureka/apps/ORDER-SERVICE
    # K8s service endpoints
    kubectl get endpoints order-service -n production
    # Test gateway route
    curl -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

    5
    1. 1What is microservices architecture?
      Beginner

      Model 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?

    2. 2What is database per service?
      Beginner

      Model 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?

    3. 3What is an API Gateway?
      Beginner

      Model 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?

    4. 4What is service discovery?
      Beginner

      Model 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?

    5. 5How do you decompose a monolith?
      Beginner

      Model 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

    5
    1. 6Explain the strangler fig pattern.
      Intermediate

      Model 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?

    2. 7How handle distributed transactions?
      Intermediate

      Model 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?

    3. 8API Gateway vs BFF (Backend for Frontend)?
      Intermediate

      Model 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?

    4. 9Eureka vs Kubernetes service discovery?
      Intermediate

      Model 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?

    5. 10What is a distributed monolith?
      Intermediate

      Model 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

    5
    1. 11Design e-commerce checkout as microservices.
      Advanced

      Model 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?

    2. 12When NOT to use microservices?
      Advanced

      Model 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?

    3. 13How migrate 500K LOC Spring monolith?
      Advanced

      Model 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?

    4. 14Service mesh vs API Gateway?
      Advanced

      Model 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?

    5. 15How ensure idempotency across services?
      Advanced

      Model 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

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    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.
    Ready to mark this lesson complete?Track your journey across the entire course.