A demo application showcasing event-driven microservices architecture built with Java Spring Boot. This project demonstrates real-world distributed system patterns including the Outbox Pattern, Saga Choreography, Decorator Pattern, and Event-Driven Architecture using RabbitMQ. The implemented services are tested with 100% unit test coverage and integration tests powered by Testcontainers.
Halil İbrahim ŞENAYDIN
- E-mail: halilsenaydin@gmail.com
- GitHub: github.com/halilsenaydin
- Linkedin: linkedin.com/in/halilsenaydin
This system manages product inventory and order reservations across independent microservices. Each service owns its data and communicates exclusively through asynchronous events — no direct service-to-service HTTP calls. The architecture is designed to be extensible, resilient, and observable.
- No shared database — each service has its own PostgreSQL instance
- No synchronous inter-service communication — all coordination through RabbitMQ events
- Outbox pattern — guarantees at-least-once event delivery even if the broker is temporarily unavailable
- Saga choreography — distributed transactions coordinated through domain events, no central orchestrator
- Rich domain model — business logic lives in entities, not anemic service layers
- Decorator pattern — cross-cutting concerns (outbox persistence) separated from core business logic
┌────────────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ product- │ │ inventory- │ │ order- │ │
│ │ service │ │ service │ │ service │ │
│ │ :8081 │ │ :8082 │ │ :8083 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┴────────────────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ RabbitMQ │ │
│ │ :5672 │ │
│ │ :15672 (UI) │ │
│ └───────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ product-db │ │ inventory-db │ │ order-db │ │
│ │ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────┘
Manages the product catalog. Handles product creation, deletion (soft delete), and image management.
Responsibilities:
- CRUD operations for products and product images
- Publishes
ProductCreatedEventandProductCreationFailedEventvia outbox - Listens for
InventoryCreationFailedEvent— retries product creation if inventory initialization fails
API: GET/POST /api/v1/products, GET /api/v1/products/{id}, POST /api/v1/products/{id}/images, DELETE /api/v1/products/{productId}/images/{imageId}
Manages stock levels for each product. Initialized automatically when a product is created.
Responsibilities:
- Initializes inventory when a
ProductCreatedEventis received - Handles stock reservation and release requests
- Publishes
InventoryCreated,InventoryCreationFailed,InventoryStockReserved,InventoryStockReservationFailed,InventoryStockReleased,InventoryStockReleaseFailedvia outbox - Failed reservations are persisted in a separate transaction (
REQUIRES_NEW) to survive rollbacks - Listens for
OrderCreatedEvent— reserves stock for the given product and quantity
Manages the order lifecycle using a saga state machine.
Order States: PENDING → STOCK_RESERVED → PAYMENT_PENDING → CONFIRMED / CANCELLED
Note: Payment service integration is not included in this demo. The saga currently completes at the
STOCK_RESERVEDstage.
Responsibilities:
- Creates orders and initiates the saga
- Listens for inventory events to advance order state
- Publishes
OrderCreated,OrderCreationFailed,OrderConfirmed,OrderCancelledvia outbox
A common library containing shared models, base classes, and utilities used across all services.
Contents:
BaseEntity— common JPA fields:id,createdDate,updatedDate,deletedDate,isActiveBaseOutbox— common outbox fields:eventType,payload,processedOutboxRepository— generic Spring Data repository withfindByProcessedFalse- Event DTOs:
ProductEvent,InventoryEvent,OrderEvent JsonUtil— Jackson serialization/deserialization utilitiesOperationResult<Entity, Response, Event>— typed result wrapper for service operations- Shared Spring configurations:
SharedRabbitMQConfig,SharedSwaggerConfig,SharedJacksonConfig
Guarantees reliable event delivery. Instead of publishing directly to RabbitMQ, each service first writes the event to its own outbox table within the same database transaction. A scheduled publisher (@Scheduled(fixedDelay = 5000)) polls the outbox and sends unpublished events to RabbitMQ.
Business Transaction:
1. Save domain entity
2. Save outbox record (same transaction)
→ Commit
Outbox Publisher (every 5 seconds):
3. Poll unprocessed outbox records
4. Publish to RabbitMQ
5. Mark as processed
This ensures no event is lost even if RabbitMQ is temporarily unavailable.
Distributed transactions are coordinated through domain events — no central orchestrator. Each service reacts to events and publishes its own, forming a chain of reactions.
product-service inventory-service order-service
| | |
|-- ProductCreatedEvent ------->| |
| |-- (inventory initialized) |
| | |
|<-- InventoryCreationFailed ---| |
| (retries product creation) | |
| | |
| |<-- OrderCreatedEvent -------------------|
| | |
| |-- InventoryStockReservedEvent --------->|
| | |--> OrderConfirmedEvent
| | |
| |-- InventoryStockReservationFailed ----->|
| | |--> OrderCancelledEvent
Compensating transactions (e.g., soft-deleting a product when inventory initialization fails) are handled by consuming failure events.
The Decorator pattern separates outbox persistence from core business logic. Each service has:
ProductManager/InventoryManager— pure domain logic, no outbox awarenessProductOutboxDecorator/InventoryOutboxDecorator— wraps the manager, adds outbox persistenceProductServiceConfig/InventoryServiceConfig— wires the decorator chain via Spring@Bean
// Core logic (no outbox)
class ProductManager implements ProductService { ... }
// Decorator (adds outbox without modifying core)
class ProductOutboxDecorator extends ProductServiceDecorator {
@Override
public OperationResult create(CreateProductRequest request) {
OperationResult result = delegate.create(request);
outboxService.save(PRODUCT_CREATED_EVENT, result.getEvent());
return result;
}
}
// Wired in config
@Bean
public ProductService productService(...) {
return new ProductOutboxDecorator(new ProductManager(...), outboxService);
}Each service publishes to its own Topic Exchange and declares its own queues. Consumer services only know the queue name — not the exchange or routing key of the publisher.
| Exchange | Routing Key | Queue |
|---|---|---|
product-exchange |
product.created |
product-created-queue |
product-exchange |
product.creation.failed |
product-creation-failed-queue |
inventory-exchange |
inventory.created |
inventory-created-queue |
inventory-exchange |
inventory.creation.failed |
inventory-creation-failed-queue |
inventory-exchange |
inventory.stock.reserved |
inventory-stock-reserved-queue |
inventory-exchange |
inventory.stock.reservation.failed |
inventory-stock-reservation-failed-queue |
inventory-exchange |
inventory.stock.released |
inventory-stock-released-queue |
inventory-exchange |
inventory.stock.release.failed |
inventory-stock-release-failed-queue |
order-exchange |
order.created |
order-created-queue |
order-exchange |
order.creation.failed |
order-creation-failed-queue |
order-exchange |
order.confirmed |
order-confirmed-queue |
order-exchange |
order.cancelled |
order-cancelled-failed-queue |
| Layer | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.5 |
| Messaging | RabbitMQ (Topic Exchange) |
| Database | PostgreSQL 16 |
| ORM | Hibernate / Spring Data JPA |
| API Docs | SpringDoc OpenAPI (Swagger UI) |
| Build | Maven |
| Containerization | Docker, Docker Compose |
| Testing | JUnit 5, Mockito, Testcontainers |
| Coverage | JaCoCo |
Note: Tests are currently implemented for
product-serviceonly.inventory-serviceandorder-servicetests are planned for a future iteration.
Every business logic class is covered with unit tests using JUnit 5 and Mockito. All dependencies are mocked — no database or message broker required.
Coverage: 100% on all business logic classes
Tested classes include:
ProductManager— all CRUD operations and exception pathsProductOutboxDecorator— verifies outboxsaveis called with correct event typeProductServiceDecorator— verifies delegation behavior including non-overridden methodsProductOutboxManager— save and saveInNewTransaction variantsProductOutboxEventPublisher— routing key resolution for each event typeProductOutboxPublisher— scheduled polling, batch processing, empty queue handlingInventoryEventConsumer— soft delete and retry logic on failed inventory creationProductMapper— entity-to-DTO and entity-to-event conversionsProduct/ProductImage— domain logic including equals/hashCode contractsRabbitMQConfig— exchange name, queue name, routing key, durabilityProductServiceConfig— correct bean wiring (OutboxDecorator wrapping Manager)
Run unit tests:
cd product-service
mvn testIntegration tests use Testcontainers to spin up real PostgreSQL and RabbitMQ containers. Tests run against a full Spring context with actual database queries and message publishing.
Covered scenarios:
ProductsControllerIT— full HTTP request/response cycle for all endpointsInventoryEventConsumerIT— consumer processes event, soft-deletes product, retries creationProductOutboxManagerIT— outbox records are persisted correctly for all save variantsProductOutboxPublisherIT— scheduled outbox polling publishes events to RabbitMQ and marks records as processed
Run integration tests:
cd product-service
mvn failsafe:integration-testRun all tests:
mvn clean verify -Pcoverage-it -DskipUTNote: Docker Desktop must be running for Testcontainers to work. Integration tests create isolated containers — your development database is never touched.
mvn clean verify -Pcoverage-allCoverage report is generated at target/site/jacoco/index.html.
- Docker Desktop
- Java 21 (for local development)
- Maven 3.9+ (for local development)
docker compose up --buildcd shared && mvn install
cd ../product-service && mvn spring-boot:run| Service | URL |
|---|---|
| product-service API | http://localhost:8081/swagger-ui.html |
| inventory-service API | http://localhost:8082/swagger-ui.html |
| order-service API | http://localhost:8083/swagger-ui.html |
| RabbitMQ Management | http://localhost:15672 (guest/guest) |
distributed-inventory-reservation-system/
├── shared/ # Shared library
│ └── src/main/java/.../shared/
│ ├── business/
│ │ ├── constants/
│ │ ├── exceptions/
│ │ ├── models/
│ │ └── utils/
│ ├── core/config/
│ ├── dataAccess/abstracts/
│ └── entities/
│ ├── abstracts/
│ └── events/
│
├── product-service/
│ └── src/main/java/.../product_service/
│ ├── api/controllers/
│ ├── business/
│ │ ├── abstracts/
│ │ ├── concretes/
│ │ ├── consumers/
│ │ ├── decorators/
│ │ ├── mapper/
│ │ ├── messaging/
│ │ └── constants/
│ ├── config/
│ ├── core/config/
│ ├── dataAccess/abstracts/
│ └── entities/
│ ├── concretes/
│ └── dtos/
│
├── inventory-service/ # Same structure as product-service
├── order-service/ # Same structure — saga state machine
└── docker-compose.yml
- Create a new Spring Boot module
- Add
sharedas a Maven dependency - Define your entity extending
BaseEntity, outbox entity extendingBaseOutbox - Implement the Manager → Decorator → Config pattern
- Declare your exchange, queues, and bindings in a
RabbitMQConfig - Add your service to
docker-compose.ymlwith its own PostgreSQL instance
- Add the event DTO to
shared/entities/events/ - Add routing key and queue constants to the relevant
*EventConstantclass inshared - Add the queue/binding to the publisher service's
RabbitMQConfig - Add the
caseto the publisher'sresolveRoutingKeyswitch - Implement the consumer's
@RabbitListenerin the consuming service
This project is licensed under the MIT License.
You are free to use, modify, and distribute this code for both personal and commercial purposes.
See the LICENSE file for full license text.