Project: Designing a Microservices System
Project: Designing a Microservices System
Every principle covered in this tutorial — bounded contexts, database-per-service, synchronous and asynchronous communication, saga coordination, cross-cutting concerns — must eventually be reconciled in a single coherent design. This lesson walks through the complete decomposition of a realistic e-commerce domain into well-bounded services, explains every decision at the level a working developer needs, and shows how the resulting services connect as runnable Spring Boot 3 components.
The Domain: an Online Marketplace
The business operates a marketplace where sellers list products, customers place orders, a payment processor charges cards, and a fulfilment team ships packages. Additional capabilities include search, notifications, and an admin reporting portal. Before writing a single class you need to map the domain using Event Storming — list every domain event in chronological order, then colour-code them by business capability to reveal natural service boundaries.
Key domain events identified:
- ProductListedByseller, ProductUpdated, ProductDeactivated
- CartItemAdded, CheckoutInitiated
- OrderPlaced, OrderConfirmed, OrderCancelled
- PaymentAuthorised, PaymentFailed, RefundIssued
- ShipmentCreated, PackageDispatched, DeliveryConfirmed
- CustomerRegistered, EmailVerified, PasswordChanged
Grouping events by the team that owns them — not by technical similarity — yields the following bounded contexts:
Decomposition into Services
Six services emerge from the event map. Each owns its own database, exposes a REST API for synchronous reads, and publishes/consumes events over Kafka for state-changing operations.
- Catalog Service — CRUD for products; Elasticsearch index for search. Publishes
ProductUpdated. - Order Service — shopping cart and order lifecycle. Publishes
OrderPlaced,OrderCancelled; listens toPaymentAuthorised,ShipmentCreated. - Payment Service — payment gateway integration. Publishes
PaymentAuthorised,PaymentFailed. - Inventory Service — stock reservation. Listens to
OrderPlaced; publishesStockReserved,StockInsufficient. - Fulfilment Service — shipping labels and carrier integration. Listens to
PaymentAuthorised; publishesPackageDispatched. - Identity Service — user accounts, JWT issuance, password management. All other services validate tokens from here.
Order Service — Skeleton
The Order Service demonstrates the canonical Spring Boot 3 structure. It exposes a REST API for the checkout flow and publishes domain events to Kafka for downstream services.
@TransactionalEventListener(phase = AFTER_COMMIT)) to guarantee exactly-once publication relative to your own DB write.
Kafka Event Contracts
Event schemas are your public API. Use Avro with a Schema Registry so incompatible changes are caught at build time, not at runtime on production. A minimal Avro schema for OrderPlaced:
The Inventory Service listens on the same topic. Its consumer is idempotent — if the same OrderPlaced message is delivered twice (Kafka guarantees at-least-once), the second attempt sees the reservation already exists and does nothing:
The Checkout Saga
Placing an order is a multi-service saga: Order → Inventory → Payment → Fulfilment. Use a choreography-based saga for this flow — no central orchestrator, each service reacts to events and publishes compensating events on failure:
- Order Service publishes
OrderPlaced. - Inventory Service reserves stock → publishes
StockReservedorStockInsufficient. - Payment Service charges card → publishes
PaymentAuthorisedorPaymentFailed. - On any failure, compensating events roll back prior steps (
StockReleased,OrderCancelled). - On full success, Fulfilment Service creates a shipment.
Cross-Service Security: JWT Propagation
The Identity Service issues a signed JWT on login. Every other service validates the token locally with the Identity Service's public key — no round-trip on each request. Spring Security 6 makes this one property:
ClientCredentials grant (machine-to-machine) so Inventory Service can audit which caller made the request.
Spring Cloud Gateway — The Single Entry Point
Expose exactly one public hostname. Spring Cloud Gateway routes requests to the correct downstream service, enforces authentication at the edge, and strips internal headers that clients should never see:
Observability Wiring
In a distributed system a single user request fans out across multiple services. Without correlated tracing you cannot reconstruct what happened. Add Micrometer Tracing with Zipkin to every service — Spring Boot 3 autoconfigures this from two dependencies:
Every log line automatically includes a traceId and spanId. Aggregate logs in a central store (ELK, Loki) and filter by traceId to follow a checkout request across Order, Inventory, Payment, and Fulfilment services in one query.
Design Decisions Recap
- Six services aligned to business capabilities, not technical layers.
- Database per service: PostgreSQL for Order/Payment/Fulfilment, MongoDB for Catalog, Redis for Inventory (fast reservation checks), MySQL for Identity.
- Async by default: state-changing operations go through Kafka; synchronous REST is reserved for queries that need an immediate response.
- Choreography saga for the checkout flow; orchestration saga would be preferable if the number of steps exceeds six or compensation logic becomes complex.
- JWT at the edge and between services — no session state anywhere.
- One gateway — clients never know service topology; internal URLs are opaque.
With this architecture in place, any single service can be redeployed, scaled horizontally, or replaced without touching the others — which is the defining promise of the microservices style.