microservices · event-driven · kuum-oss
kuum/fou.
// e-commerce notification microservice

Event-driven notification service for an e-commerce platform.
Order events flow through RabbitMQ into a durable consumer pipeline — persisted to PostgreSQL, reported to S3, with graceful degradation when upstream services fail. JWT-secured REST API, Testcontainers integration tests, LocalStack S3 in Docker.

"Don't let S3 take down your order flow — degrade gracefully, retry asynchronously."
0svc
docker services
0×
integration tests
0min
S3 presigned URL TTL
0
lost messages on restart
01 system architecture
LAYER I · REST
Order Producer
Spring MVC · JWT Auth
  • POST /orders → publishes to RabbitMQ
  • GET /orders/user/{id} → N+1-safe query
  • GET /orders/{id}/report → presigned URL
  • JWT filter on all non-auth routes
  • RFC 7807 Problem Details on errors
  • @PreAuthorize ownership check
I
LAYER II · MESSAGING
RabbitMQ Pipeline
Spring AMQP · Durable Queue · DLQ
  • Durable exchange + queue
  • Dead-letter queue for failed messages
  • JSON serialization via Jackson
  • Exponential backoff on producer
  • Idempotency check in consumer
  • Auto-ack with DLQ on rejection
II
LAYER III · STORAGE
Persistence + S3
JPA · Hibernate · LocalStack
  • Order + Notification saved in one tx
  • S3 upload outside transaction
  • PENDING_UPLOAD fallback on S3 fail
  • @Scheduled retry every 5 min
  • Flyway schema versioning
  • JOIN FETCH solves N+1
III
02 event flow
POST /orders
     │
     ▼
OrderController ──► OrderProducer ──► RabbitMQ (orders.queue, durable)
                                              │
                                              ▼
                                       OrderConsumer┌─────────────────────┼──────────────────────┐
                         ▼                   ▼                     ▼
               OrderRepository   NotificationRepository   S3ReportService
                         │                   │                     │
                         ▼                   ▼              ┌──────┴──────┐
                   PostgreSQL        PostgreSQL             │   success   │   failure
                                                            │             │
                                                            ▼             ▼
                                                        SENT    PENDING_UPLOAD@Scheduled every 5 min ──► S3RetryScheduler
03 design decisions
architecture decisions · rationale
topic decision why
Hibernate N+1 JOIN FETCH in JPQL Single SQL loads orders + notifications instead of 1+N queries
S3 fallback PENDING_UPLOAD status DB commit before S3 — upload failure never loses the order
Idempotency existsById() check Duplicate RabbitMQ delivery silently skipped, no duplicate orders
Transaction split TransactionTemplate S3 call outside TX — connection not held open during slow upload
DLQ orders.dlq Poison messages don't loop forever — inspectable, replayable
Retry scheduler @Scheduled fixed delay S3 recovers → all PENDING_UPLOAD notifications auto-healed
Auth JWT + Spring Security Stateless, ownership-checked — users only see their own orders
04 tech stack
core
Java 21 Spring Boot 3 RabbitMQ PostgreSQL Spring Data JPA Hibernate Flyway
storage & messaging
AWS SDK v2 LocalStack S3 Spring AMQP Jackson Spring Security JWT (jjwt)
testing & infra
Testcontainers LocalStack TC Awaitility Docker Compose Prometheus Grafana Spring Actuator
05 quick start
STEP 1 · SETUP
Start Stack
Docker · Java 21 · Maven
  • git clone kuum-oss/fou
  • docker compose up --build -d
  • PostgreSQL → :5432
  • RabbitMQ UI → :15672 (guest/guest)
  • LocalStack S3 → :4566
  • App → :8080
1
STEP 2 · AUTH
Get Token
JWT · POST /auth/token
  • POST /auth/token {"userId":"user-123"}
  • returns {"token":"eyJ..."}
  • use as Bearer in all requests
  • token tied to userId
  • ownership enforced on GET /orders
2
STEP 3 · USE
Create Order
REST → RabbitMQ → S3
  • POST /orders → 202 + orderId
  • consumer processes async
  • GET /orders/user/{id} → orders + notifications
  • GET /orders/{id}/report → presigned URL
  • URL valid 15 minutes
3
terminal · full flow
$ git clone https://github.com/kuum-oss/fou && cd fou
$ docker compose up --build -d
[+] Running 4/4 — postgres · rabbitmq · localstack · notifyhub
 
# get JWT token
$ curl -X POST localhost:8080/auth/token \
    -H "Content-Type: application/json" \
    -d '{"userId":"user-123"}'
{"token":"eyJhbGciOiJIUzI1NiJ9..."}
 
# create order → published to RabbitMQ
$ curl -X POST localhost:8080/orders \
    -H "Authorization: Bearer eyJ..." \
    -d '{"userId":"user-123","amount":99.99}'
{"orderId":"550e8400...","message":"Order accepted and queued"}
 
# get S3 presigned report URL
$ curl localhost:8080/orders/550e8400.../report \
    -H "Authorization: Bearer eyJ..."
{"presignedUrl":"http://localhost:4566/notifyhub-reports/order_550e8400..._report.txt?..."}
$
06 integration tests
Testcontainers · real infra · no mocks ✓ ALL PASS
test containers what it verifies result
whenOrderCreated_thenOrderAndNotificationSavedInDb Postgres · RabbitMQ REST → queue → consumer → DB · full async flow with Awaitility ✓ PASS
whenMessagePublishedDirectly_thenConsumedAndPersistedToPostgres Postgres · RabbitMQ Producer → durable queue → consumer saves Order + Notification ✓ PASS
whenReportUploaded_thenFileExistsInS3AndPresignedUrlIsReturned Postgres · RabbitMQ · LocalStack S3 upload via LocalStack · HeadObject confirms file · presigned URL generated ✓ PASS
whenFetchingOrdersForUser_thenNotificationsLoadedWithoutN1 Postgres · RabbitMQ · LocalStack 2 orders × 2 notifications — JOIN FETCH loads all without LazyInitializationException ✓ PASS
whenS3Unavailable_thenNotificationSavedWithPendingUploadStatus Postgres · RabbitMQ · LocalStack Bucket deleted → upload fails → notification persisted as PENDING_UPLOAD ✓ PASS
5
integration tests
3
real containers
0
infrastructure mocks
ESC / CLICK TO CLOSE ×
07 proof · it runs
startup Spring Boot up — Flyway, HikariCP, RabbitMQ, S3 in 4.3s
Spring Boot startup log
runtime Flyway validated · JOIN FETCH SQL · S3RetryScheduler idle
Flyway and scheduler log
api JWT → POST /orders → GET with notifications → RFC 7807 on bad UUID
curl API proof
build ./mvnw clean package · 28 files · BUILD SUCCESS · 3.4s
Maven build success