Spring Boot REST API that demonstrates an event-driven order lifecycle (order -> payment -> inventory -> notification) using Kafka. It focuses on reliability patterns (transactional outbox), idempotency, and asynchronous processing while keeping the business domain small and testable. The service uses PostgreSQL for persistence, Redis for read caching, and JWT for stateless authentication.
- Transactional outbox pattern: domain events are persisted to
outbox_eventsand published to Kafka by a scheduled job - Asynchronous order lifecycle across bounded contexts (order, payment, inventory, notification) using Kafka consumers and event chaining
- Idempotent payment processing via unique idempotency keys (both internal payment simulation and Stripe webhook processing)
- Redis-backed caching for read-heavy endpoints with explicit cache eviction on state transitions
- Optimistic locking (
@Version) on core entities to protect against lost updates - Stateless JWT auth (Spring Security filter) with role-based admin endpoints
- Integration tests using Testcontainers (PostgreSQL, Kafka, Redis) plus Awaitility for async assertions
Separation of concerns
- HTTP layer: controllers for auth, products, orders, Stripe webhook
- Domain services: business logic per module (order/payment/inventory/notification)
- Persistence: PostgreSQL + Flyway migrations
- Messaging: Kafka topics as the integration boundary
- Caching: Redis via Spring Cache
Event flow (happy path)
POST /api/orderspersists an order and writesOrderCreatedEventto the outboxPATCH /api/orders/{id}/paywritesOrderPaymentRequestedEventto the outbox- Outbox publisher sends events to Kafka (topic determined from event class name)
- Payment consumer processes request and publishes
PaymentSucceededEventorPaymentFailedEvent - Order consumer updates order state and publishes follow-up events (
OrderPaidEvent,OrderCancelledEvent) - Inventory consumer reserves stock on
OrderPaidEventand publishesInventoryReservedEvent - Order consumer completes the order on
InventoryReservedEventand publishesOrderCompletedEvent - Notification consumer reacts to
OrderCompletedEvent(currently logs)
Stripe checkout flow (async)
POST /api/orders/{id}/payments/striperecords astripe_checkout_requestsrow and emitsOrderStripeCheckoutRequestedEvent- A Kafka consumer attempts to create a Stripe Checkout session and updates the request as
READYwithcheckoutUrlorFAILED GET /api/orders/{id}/payments/stripereturns the current request status for pollingPOST /api/stripe/webhookdeduplicates webhook deliveries and emits payment result events
- Outbox publishing is decoupled from request transactions: events are written to Postgres first, then a scheduled job publishes and marks
published_at - Kafka topic routing is derived from event type naming (
.order.,.payment.,.inventory.) to keep producers simple - Webhook deduplication uses a persisted
stripe_webhook_eventstable and a unique payment idempotency key per Stripe event id - Internal payment processing is intentionally deterministic (amount vs expected amount) to keep the async event choreography testable
- Caching is scoped to specific reads (
orders,productById) with TTLs and explicit eviction on state changes
src/
main/
java/com/example/ordersystem/
config/ # Security, Redis cache manager, OpenAPI
inventory/ # Product catalog + stock reservation
notification/ # Notification listener/service (log-based)
order/ # Order API, lifecycle, Stripe checkout request tracking
payment/ # Payment processing + Stripe webhook handling
shared/ # Outbox, domain events, security, exceptions, base entities
resources/
application.yml # runtime config (DB, Kafka, Redis)
db/migration/ # Flyway migrations
test/
java/com/example/ordersystem/ # Integration tests (Testcontainers + Awaitility)
resources/
application-test.properties # test-only config
| Category | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.5.x |
| API | Spring Web (REST), Spring Validation |
| Database | PostgreSQL, Flyway |
| ORM | Spring Data JPA (Hibernate) |
| Messaging | Apache Kafka (Spring Kafka) |
| Caching | Redis (Spring Cache) |
| Auth | Spring Security + JWT (JJWT) |
| API docs | springdoc-openapi (Swagger UI) |
| Mapping | MapStruct |
| Build / format | Maven Wrapper, Spotless (google-java-format) |
| Testing | JUnit 5, Testcontainers (Postgres/Kafka/Redis), Awaitility |
- Java 21
- Docker (for PostgreSQL/Redis/Kafka via Compose)
Start infrastructure:
docker compose up -dCreate a .env file (see Configuration section) and run the app:
./mvnw spring-boot:runWindows:
.\mvnw.cmd spring-boot:runUseful local URLs:
- API:
http://localhost:8080 - Swagger UI:
http://localhost:8080/swagger-ui/index.html - Kafka UI:
http://localhost:8081
Provide running instances of PostgreSQL, Redis, and Kafka, then set the required environment variables and start:
./mvnw spring-boot:runThe application loads environment variables from .env on startup (via dotenv-java) and fails fast when required values are missing.
Required:
SECRET_KEY- JWT signing key (HMAC)DB_NAME- PostgreSQL database nameDB_USER- PostgreSQL usernameDB_PASSWORD- PostgreSQL passwordDB_PORT- local port mapped to Postgres (used byapplication.yml)
Optional (Stripe):
STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_CURRENCY(default:pln)STRIPE_SUCCESS_URL(default points to Swagger UI)STRIPE_CANCEL_URL(default points to Swagger UI)
POST /api/auth/registerPOST /api/auth/loginGET /api/auth/meGET /api/auth/profile
GET /api/ordersGET /api/orders/adminPOST /api/ordersGET /api/orders/{id}PATCH /api/orders/{id}/payPOST /api/orders/{id}/payments/stripeGET /api/orders/{id}/payments/stripe
GET /api/productsGET /api/products/{id}POST /api/products
POST /api/stripe/webhook
GET /health
- Integration tests: exercise HTTP endpoints and async Kafka-driven workflows using Testcontainers for PostgreSQL, Kafka, and Redis
- Async assertions: Awaitility is used to wait for state transitions caused by background consumers/outbox publishing
Run tests:
./mvnw test- Designing an event-driven workflow with clear domain boundaries and Kafka consumer groups
- Implementing a transactional outbox publisher to avoid dual-write problems
- Making async processing testable with Testcontainers + Awaitility
- Applying idempotency patterns for payments and webhook retries
- Using Redis caching safely with explicit eviction on writes