## https://sploitus.com/exploit?id=CF35ABEF-D185-56B0-901F-DB28435B4751
# Event-Driven E-Commerce Saga POC
This project is a small event-driven e-commerce proof of concept built with
Java, Spring Boot, Apache Kafka, Maven, JPA, and H2.
It demonstrates how an order can move through payment and inventory processing
without one service directly calling another. Services communicate by
publishing and consuming Kafka events. When inventory reservation fails after
payment succeeds, the system starts a compensating action and refunds the
payment.
> This is an educational POC, not a production-ready checkout system. The
> [Current Limitations](#current-limitations-and-improvements) section explains
> the main gaps and is especially useful for interview preparation.
## Quick Revision
Your current system already has:
Order Service
Payment Service
Inventory Service
Kafka
Saga
Retry
DLQ
Compensation
Current architecture:
Spring Boot Parent (4.0.5)
โ
ecommerce-poc
โ
โโโ common-events
โโโ order-service
โโโ payment-service
โโโ inventory-service
โโโ api-gateway
Execution Steps:
On local ->
Install Docker Desktop
Open docker desktop, check if the container is up & running
Open 5 terminals in the project (intellij)
on the 1st terminal (inside /ecommerce-poc) -> docker compose up -d
Check zookeeper and kafka are running (using docker ps command), after that on remaining 4 terminals run below:
1. API Gateway Service terminal (inside /api-gateway) -> mvn spring-boot:run
2. Order Service terminal (inside /order-service) -> mvn spring-boot:run
3. Payment Service terminal (inside /payment-service) -> mvn spring-boot:run
4. Inventory Service terminal (inside /inventory-service) -> mvn spring-boot:run
Once running on 1st terminal , run - curl -X POST http://localhost:8081/orders
After that open each terminal and see message like Order Received, Payment Completed, Inventory Processed. On API Gateway terminal you
will see Netty started at 8080.
Java 17 โ
Spring Boot 3.5.0 โ
Spring Cloud 2025.0.0 โ
Multi-module Maven โ
common-events โ
order-service โ
payment-service โ
inventory-service โ
Kafka โ
Saga Pattern โ
Retry โ
DLQ โ
Compensation โ
Idempotency โ
BUILD SUCCESS โ
Client
โ
โผ
API Gateway
โ
โผ
Order Service
โ
โผ
Kafka
โโโ Payment Service
โโโ Inventory Service
Observability Stack
-------------------
Micrometer
โ
Prometheus
โ
Grafana
You can now say:
We instrumented business metrics, not just infrastructure metrics.
Infrastructure metrics:
CPU
Memory
JVM
Latency
HTTP Throughput
Business metrics:
Orders Created
Orders Completed
Orders Cancelled
Payments Success
Payments Failed
Payments DLQ
Inventory Reserved
Inventory Failed
You now have:
Business Metrics โ
Technical Metrics โ
Actuator โ
Micrometer โ
API Gateway โ
Kafka Saga โ
Order Service
Payment Service
Inventory Service
API Gateway
โ
โผ
Micrometer
โ
โผ
Prometheus
โ
โผ
Grafana
Supported by:
Spring Boot 3.5
Spring Cloud 2025
Kafka
Saga Pattern
Retry
DLQ
Compensation
Idempotency
H2 Database
Multi-module Maven
API Gateway
Architecture -
Client
โ
โผ
API Gateway
โ
โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โผ โผ
Order Service Payment Service
โ โ
โ โผ
โ Circuit Breaker
โ โ
โผ โผ
Kafka โโโโโโโโโโโโโโโโโโบ Inventory Service
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Saga Events
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Redis Cache
โฒ
โ
Order Service
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Micrometer
โ
โผ
Prometheus
โ
โผ
Grafana
Monitoring
----------
Micrometer
โผ
Prometheus
โผ
Grafana
Patterns Used
Pattern Where
Saga Payment + Inventory + Compensation
Event Driven Kafka Topics
Retry Payment failures
DLQ Poison messages
Idempotency Duplicate event handling
API Gateway Entry point
Eventual Consistency Order lifecycle
- **Architecture:** event-driven microservices
- **Pattern demonstrated:** choreography-based Saga
- **Message broker:** Apache Kafka
- **Entry point:** `POST /orders`
- **Services:** Order, Payment, Inventory
- **Persistence:** H2 is configured only for Order Service
- **Success path:** `CREATED -> PAID -> COMPLETED`
- **Payment failure:** `CREATED -> FAILED`
- **Inventory failure:** `CREATED -> PAID -> CANCELLED`
- **Compensation:** Inventory failure triggers a payment refund event
- **Retry strategy:** Payment failures are retried up to three times, then sent
to a dead-letter topic
## Architecture
```mermaid
flowchart LR
Client["Client"] -->|"POST /orders"| Order["Order Service :8080"]
Order -->|"order-events"| Kafka["Kafka :9092"]
Kafka -->|"order-events"| Payment["Payment Service :8081"]
Payment -->|"payment-success-events"| Kafka
Payment -->|"payment-failed-events"| Kafka
Kafka -->|"payment-success-events"| Inventory["Inventory Service :8082"]
Kafka -->|"payment success/failure"| Order
Inventory -->|"inventory-success-events"| Kafka
Inventory -->|"inventory-failed-events"| Kafka
Kafka -->|"inventory result"| Order
Kafka -->|"inventory-failed-events"| Payment
Payment -->|"payment-refund-events"| Kafka
Kafka -->|"payment-refund-events"| Order
Order --> H2[("H2 orders table")]
```
### Why This Is a Choreography-Based Saga
There is no central saga orchestrator. Each service reacts to an event and
decides which event to publish next:
1. Order Service creates the order and publishes `order-events`.
2. Payment Service reacts and publishes a payment result.
3. Inventory Service reacts to payment success and publishes an inventory
result.
4. Payment Service reacts to inventory failure by issuing a refund event.
5. Order Service listens to result events and updates the order status.
This reduces direct service coupling, but the overall workflow becomes
distributed across multiple consumers and topics.
## Modules
| Module | Port | Responsibility |
| --- | ---: | --- |
| `common-events` | N/A | Shared Java event contracts used for Kafka JSON messages |
| `order-service` | `8080` | Exposes the order API, persists orders, starts the saga, and updates final status |
| `payment-service` | `8081` | Simulates payment success/failure, retries failed payments, handles the DLQ, and simulates refunds |
| `inventory-service` | `8082` | Simulates inventory reservation and publishes success/failure |
## End-to-End Code Flow
### 1. Create an Order
The client calls:
```http
POST /orders
```
`OrderController.createOrder()`:
1. Generates a UUID as the order ID.
2. creates an `Order` entity with status `CREATED`;
3. saves it through `OrderRepository`;
4. creates an `OrderCreatedEvent`;
5. publishes the event to `order-events`;
6. returns the order ID.
### 2. Process Payment
`PaymentConsumer.consume()` listens to `order-events`.
Payment is simulated with `Math.random()`:
- approximately 50% chance: publish `PaymentFailedEvent` to
`payment-failed-events`;
- approximately 50% chance: publish the original order event to
`payment-success-events`.
### 3A. Payment Succeeds
Two independent Kafka consumer groups receive `payment-success-events`:
- Order Service, group `order-group`, marks the order `PAID`;
- Inventory Service, group `inventory-group`, starts inventory reservation.
Kafka delivers one copy per consumer group, so both services receive the event.
Inventory is also simulated with a 50/50 random result:
- success: publish `inventory-success-events`;
- failure: publish `InventoryFailedEvent` to `inventory-failed-events`.
### 3B. Payment Fails
`payment-failed-events` is consumed by two groups:
- Order Service (`order-group`) marks the order `FAILED`;
- Payment Service retry listener (`retry-group`) attempts the payment again.
After three retry counts, the event is published to `payment-dlq`. The
`dlq-group` listener currently logs the dead-letter event.
### 4A. Inventory Succeeds
Order Service consumes `inventory-success-events` and changes the order status
to `COMPLETED`.
```text
CREATED -> PAID -> COMPLETED
```
### 4B. Inventory Fails and the Saga Compensates
1. Inventory Service publishes `inventory-failed-events`.
2. Payment Service consumes it and simulates refunding the payment.
3. Payment Service publishes `payment-refund-events`.
4. Order Service consumes the refund event and marks the order `CANCELLED`.
```text
CREATED -> PAID -> CANCELLED
```
This refund is a **compensating transaction**: it semantically reverses an
earlier successful step instead of using a distributed database rollback.
## Kafka Topics
| Topic | Producer | Consumer group(s) | Payload | Purpose |
| --- | --- | --- | --- | --- |
| `order-events` | Order Service | `payment-group` | `OrderCreatedEvent` | Starts payment processing |
| `payment-success-events` | Payment Service | `order-group`, `inventory-group` | `OrderCreatedEvent` | Marks order paid and starts inventory |
| `payment-failed-events` | Payment Service | `order-group`, `retry-group` | `PaymentFailedEvent` | Marks order failed and triggers retry |
| `inventory-success-events` | Inventory Service | `order-group` | `OrderCreatedEvent` | Completes the order |
| `inventory-failed-events` | Inventory Service | `payment-group` | `InventoryFailedEvent` | Starts payment compensation |
| `payment-refund-events` | Payment Service | `order-group` | `PaymentRefundEvent` | Marks the compensated order cancelled |
| `payment-dlq` | Payment Service | `dlq-group` | `PaymentFailedEvent` | Stores/logs exhausted payment retries |
| `payment-events` | Payment retry code | None | `OrderCreatedEvent` | Currently unused due to a topic mismatch |
## Order State Transitions
```mermaid
stateDiagram-v2
[*] --> CREATED: POST /orders
CREATED --> PAID: payment-success-events
CREATED --> FAILED: payment-failed-events
PAID --> COMPLETED: inventory-success-events
PAID --> CANCELLED: inventory failure, then refund
```
The statuses are plain strings in the current code. In a production system,
they should normally be represented by an enum and protected by valid
transition rules.
## Event Contracts
### `OrderCreatedEvent`
Fields:
- `eventId`
- `orderId`
- `userId`
- `amount`
- `timestamp`
Only `orderId` is populated by the current controller.
### `PaymentFailedEvent`
Fields:
- `orderId`
- `reason`
- `retryCount`
### `PaymentCompletedEvent`
Contains `orderId`. It exists in `common-events` but is not used by the current
workflow.
### `InventoryFailedEvent`
Contains `orderId` and starts compensation.
### `PaymentRefundEvent`
Contains `orderId` and tells Order Service that compensation completed.
## Project Structure and File Responsibilities
Generated Maven wrapper files, `.gitignore`, `.gitattributes`, `HELP.md`, IDE
metadata, and downloaded `Zone.Identifier` files are omitted below because they
do not implement business behavior.
```text
ecommerce-poc/
|-- pom.xml
|-- docker-compose.yml
|-- common-events/
| |-- pom.xml
| `-- src/main/java/com/ecommerce/events/
| |-- OrderCreatedEvent.java
| |-- PaymentCompletedEvent.java
| |-- PaymentFailedEvent.java
| |-- InventoryFailedEvent.java
| `-- PaymentRefundEvent.java
|-- order-service/
| |-- pom.xml
| `-- src/
| |-- main/
| | |-- java/com/ecommerce/order_service/
| | | |-- OrderServiceApplication.java
| | | |-- controller/OrderController.java
| | | |-- consumer/PaymentResultConsumer.java
| | | |-- entity/Order.java
| | | |-- repository/OrderRepository.java
| | | `-- events/OrderCreatedEvent.java
| | `-- resources/
| | |-- application.properties
| | `-- application.yml
| `-- test/java/.../OrderServiceApplicationTests.java
|-- payment-service/
| |-- pom.xml
| `-- src/
| |-- main/
| | |-- java/com/ecommerce/payment_service/
| | | |-- PaymentServiceApplication.java
| | | `-- consumer/
| | | |-- PaymentConsumer.java
| | | `-- RefundConsumer.java
| | `-- resources/
| | |-- application.properties
| | `-- application.yml
| `-- test/java/.../PaymentServiceApplicationTests.java
`-- inventory-service/
|-- pom.xml
`-- src/
|-- main/
| |-- java/com/ecommerce/inventory_service/
| | |-- InventoryServiceApplication.java
| | `-- consumer/InventoryConsumer.java
| `-- resources/
| |-- application.properties
| `-- application.yml
`-- test/java/.../InventoryServiceApplicationTests.java
```
### Root Files
- `pom.xml`: Maven aggregator listing all four modules and Java 17 compiler
settings.
- `docker-compose.yml`: starts one ZooKeeper node and one Kafka broker, exposing
Kafka on `localhost:9092`.
### `common-events`
- `pom.xml`: builds the shared event JAR and includes Jackson for JSON support.
- `OrderCreatedEvent.java`: order payload passed through the happy path.
- `PaymentFailedEvent.java`: payment error details and retry counter.
- `PaymentCompletedEvent.java`: currently unused payment-success DTO.
- `InventoryFailedEvent.java`: inventory failure payload.
- `PaymentRefundEvent.java`: refund-completed payload.
### `order-service`
- `OrderServiceApplication.java`: Spring Boot entry point. Its explicit
`scanBasePackages = "com.ecommerce"` scans both service and shared packages.
- `OrderController.java`: implements `POST /orders`, stores the initial order,
and publishes `order-events`.
- `PaymentResultConsumer.java`: listens to payment, inventory, and refund result
topics and updates order statuses.
- `Order.java`: JPA entity mapped to the `orders` table.
- `OrderRepository.java`: Spring Data JPA repository for CRUD operations.
- `events/OrderCreatedEvent.java`: duplicate local event class; the controller
actually imports the shared `com.ecommerce.events.OrderCreatedEvent`.
- `application.yml`: configures port `8080`, H2/JPA, and Kafka serializers,
deserializers, and consumer defaults.
- `application.properties`: sets the Spring application name.
- `OrderServiceApplicationTests.java`: only verifies that the Spring context
loads.
### `payment-service`
- `PaymentServiceApplication.java`: Spring Boot entry point.
- `PaymentConsumer.java`: processes new orders, randomly succeeds or fails
payments, retries failures, and consumes the DLQ.
- `RefundConsumer.java`: reacts to inventory failure and publishes the refund
event.
- `application.yml`: configures port `8081` and Kafka.
- `application.properties`: sets the Spring application name.
- `PaymentServiceApplicationTests.java`: context-load smoke test.
### `inventory-service`
- `InventoryServiceApplication.java`: Spring Boot entry point.
- `InventoryConsumer.java`: consumes payment success, randomly simulates stock
reservation, and publishes the inventory result.
- `application.yml`: configures port `8082` and Kafka.
- `application.properties`: sets the Spring application name.
- `InventoryServiceApplicationTests.java`: context-load smoke test.
## Technology Choices
| Technology | Role |
| --- | --- |
| Java 17 | Application language and compilation target |
| Spring Boot | Service bootstrapping and configuration |
| Spring Kafka | Kafka producers and `@KafkaListener` consumers |
| Spring Web MVC | Order Service REST endpoint |
| Spring Data JPA | Order persistence abstraction |
| H2 | In-memory development database |
| Apache Kafka | Asynchronous event transport |
| ZooKeeper | Coordination for the Kafka image used by this POC |
| Maven | Multi-module build and dependency management |
| Docker Compose | Local Kafka/ZooKeeper infrastructure |
Recommended Dashboard Panels
Business Metrics
Orders Created
sum(orders_total)
Orders Completed
sum(orders_completed_total)
Orders Cancelled
sum(orders_cancelled_total)
Payment Metrics
Payment Success
sum(payments_success_total)
Payment Failed
sum(payments_failed_total)
Payment Retry
sum(payments_retry_total)
Payment DLQ
sum(payments_dlq_total)
Inventory Metrics
Inventory Reserved
sum(inventory_reserved_total)
Inventory Failed
sum(inventory_failed_total)
Throughput Panels (Interview Gold)
Instead of totals, show rates.
Orders Per Minute
rate(orders_total[5m]) * 60
Visualization:
Time Series
Payment Failure Rate
rate(payments_failed_total[5m]) * 60
Retry Rate
rate(payments_retry_total[5m]) * 60
JVM Metrics
These are useful because interviewers often ask:
"How would you monitor JVM applications?"
Heap Usage
jvm_memory_used_bytes
CPU Usage
process_cpu_usage
HTTP Requests
http_server_requests_seconds_count
## Prerequisites
- Java 17
- Maven 3.9+
- Docker with Docker Compose
- `curl`, Postman, or another HTTP client
Check the tools:
```bash
java -version
mvn -version
docker --version
docker compose version
```
## Build and Run
### 1. Start Kafka
From the repository root:
```bash
docker compose up -d
```
Check the containers:
```bash
docker compose ps
```
### 2. Build All Modules
```bash
mvn clean install
```
This builds `common-events` first so its JAR is available to the three
services.
### 3. Start the Services
Open three terminals from the repository root.
```bash
mvn -f order-service/pom.xml spring-boot:run
```
```bash
mvn -f payment-service/pom.xml spring-boot:run
```
```bash
mvn -f inventory-service/pom.xml spring-boot:run
```
### 4. Create an Order
```bash
curl -X POST http://localhost:8080/orders
```
The response is the generated order ID:
```text
4edbbcf8-2578-4f44-a124-22125b68d614
```
Watch the three service logs to follow the event chain. Because payment and
inventory use random outcomes, repeated requests can demonstrate success,
failure, retry, DLQ, and compensation paths.
### 5. Stop Infrastructure
```bash
docker compose down
```
## Testing
Run all tests:
```bash
mvn test
```
The current tests are only Spring context smoke tests. Valuable additions would
include:
- controller tests for order creation;
- repository tests for status persistence;
- unit tests for each listener branch;
- embedded-Kafka integration tests for topic-to-topic flows;
- end-to-end tests for happy path, payment failure, inventory compensation,
retries, duplicate events, and out-of-order events.
## Configuration Notes
- Kafka is expected at `localhost:9092`.
- JSON values use Spring Kafka's `JsonSerializer` and `JsonDeserializer`.
- `spring.json.trusted.packages: "*"` is convenient for a POC but too permissive
for production.
- `auto-offset-reset: earliest` lets a new consumer group read existing records
from the start.
- Explicit `groupId` values on `@KafkaListener` take precedence for those
listeners.
- Kafka topics are not declared in this repository. The demo relies on broker
auto-topic creation.
- The H2 database is in memory, so order data is lost when Order Service stops.
## Current Limitations and Improvements
These are strong interview discussion points because they show that you can
distinguish a teaching demo from a production design.
### Correctness Issues in the Current Code
1. **Retry success uses the wrong topic.**
`PaymentConsumer.retry()` publishes successful retries to `payment-events`,
but Inventory Service and Order Service listen to
`payment-success-events`. A retry success therefore does not continue the
saga. It should publish to `payment-success-events`.
2. **A failure is immediately visible even while retries continue.**
Order Service consumes the first `payment-failed-events` message and marks
the order `FAILED`, while Payment Service may still retry it. A proper state
model might use `PAYMENT_RETRYING` and only mark `FAILED` after retries are
exhausted.
3. **The order and event publication are not atomic.**
The database save can succeed while Kafka publication fails, leaving a
`CREATED` order that never progresses. The transactional outbox pattern
would solve this reliably.
4. **No idempotency protection is active.**
Kafka messages may be delivered more than once. Consumers should store event
IDs or processed operation IDs before applying side effects. The
`processedOrders` set in Inventory Service is declared but never used, and
an in-memory set would not survive restarts anyway.
5. **Missing orders throw an exception.**
Order listeners use `findById(...).orElseThrow()`. Out-of-order delivery,
data loss, or a malformed event can cause repeated listener failures.
6. **The H2 configuration appears incorrectly nested.**
In Order Service, `jpa` and `h2` are nested under `spring.datasource`.
Standard Spring Boot properties are `spring.jpa.*` and `spring.h2.*`, so
those sections should be siblings of `datasource`.
### Design and Production Gaps
- Random outcomes make behavior nondeterministic and tests unreliable.
- Payment and inventory do not persist their own state.
- The refund is only a log plus an event; no actual payment record is reversed.
- Event fields such as `eventId`, `userId`, `amount`, and `timestamp` are not
populated.
- `double` should not represent money; use `BigDecimal` plus a currency.
- There is no event schema versioning or compatibility strategy.
- There are no explicit topic definitions, partitions, replication settings,
retention policies, or message keys.
- Producers do not assign `orderId` as the Kafka key, so ordering for one order
is not guaranteed across partitions.
- String statuses should be an enum with validated state transitions.
- `System.out.println` should be replaced by structured logging.
- There is no observability: add correlation IDs, metrics, tracing, and alerts.
- There is no authentication, authorization, input validation, or API error
model.
- There is no retry backoff; immediate republishing can create a hot loop.
- The DLQ listener only logs and has no replay or operational recovery process.
- Event contracts are tightly coupled through a shared Java JAR. Alternatives
include Avro/Protobuf/JSON Schema with a schema registry.
- `OrderCreatedEvent` is duplicated inside Order Service, and
`PaymentCompletedEvent` is unused.
- The service POMs use Spring Boot as their direct parent instead of inheriting
from the root aggregator, which reduces centralized dependency management.
- ZooKeeper-based Kafka is suitable for this image, while modern deployments
commonly use Kafka's KRaft mode.
## Production-Grade Evolution
A practical improvement order would be:
1. Fix the payment retry topic and formalize order statuses.
2. Add deterministic unit and embedded-Kafka integration tests.
3. Use Kafka keys, idempotent consumers, and persisted processed-event records.
4. Add a transactional outbox to each service that changes a database and
publishes an event.
5. Persist payment and inventory operations in service-owned databases.
6. Add retry backoff, error handlers, DLQ metadata, and replay tooling.
7. Introduce schema versioning and remove duplicate/unused contracts.
8. Add structured logs, correlation IDs, metrics, and distributed tracing.
9. Add security, validation, container images, health checks, and deployment
manifests.
## Interview Talking Points
### Why Kafka Instead of Direct REST Calls?
Kafka decouples producers from consumers, allows independent consumer groups,
supports replay, and handles traffic asynchronously. The trade-offs are
eventual consistency, more operational complexity, harder debugging, and the
need for idempotency and schema governance.
### Why Not Use a Distributed Transaction?
The services should own separate data and remain independently deployable.
Two-phase commit across services and Kafka is difficult to scale and reduces
availability. A Saga accepts eventual consistency and uses compensating
business actions.
### Choreography vs Orchestration
- **Choreography:** services react to events, as in this project. It is simple
for small flows but can become difficult to understand as workflows grow.
- **Orchestration:** a central saga component commands each step and tracks the
state machine. It improves workflow visibility but adds a central dependency.
### What Delivery Guarantee Should You Assume?
Design consumers for **at-least-once delivery**. A consumer may receive the
same event more than once, especially around crashes and offset commits.
Business operations must therefore be idempotent.
### How Would You Prevent Lost Events?
Use the transactional outbox pattern:
1. save business state and an outbox row in one database transaction;
2. asynchronously publish unsent outbox rows to Kafka;
3. mark rows as published;
4. make consumers idempotent because publishing can still repeat.
### How Should Events Be Partitioned?
Use `orderId` as the Kafka message key. Events for one order then go to the same
partition and retain partition-level ordering, while different orders can be
processed in parallel.
### What Happens When Multiple Consumer Groups Subscribe?
Each group receives its own copy of every event. Within one group, Kafka splits
partitions among consumer instances. This is why Order Service and Inventory
Service can both consume `payment-success-events`.
## Thirty-Second Project Explanation
> This is a Java 17 and Spring Boot event-driven e-commerce POC that implements
> a choreography-based Saga over Kafka. Order Service persists an order and
> publishes an order-created event. Payment Service processes it and emits a
> success or failure. On success, Inventory Service attempts reservation. An
> inventory failure triggers a compensating refund through Payment Service,
> after which Order Service cancels the order. The project also demonstrates
> consumer groups, retries, and a dead-letter topic. For production, I would add
> an outbox, idempotency, persisted service state, schema versioning, proper
> retry/backoff, observability, and stronger tests.