Event-Driven Order Processing Platform
Python · Django · PostgreSQL · RabbitMQ · Docker · Kubernetes
An e-commerce order flow split into independent Order, Stock and Payment services. Services communicate asynchronously through RabbitMQ, coordinated with the Saga pattern and made reliable with the Outbox Pattern.
Overview
The platform models a realistic e-commerce backend: an Order service that owns orders, a Stock service that owns inventory, and a Payment service that owns payments. They don't call each other directly — they exchange messages over RabbitMQ, so each service can evolve and scale independently. A Kong API gateway fronts the services, and a small Next.js demo app exercises the flow.
Problem
Distributed systems introduce problems monoliths don't have: how do you keep multiple services consistent when a request spans several of them? What happens if a service is down mid-order? And how do you publish events without risking the classic dual-write problem — writing to the database and the broker, where one can succeed and the other fail? This project implements the standard answers — Saga orchestration and the Outbox Pattern — rather than hand-waving around them.
Architecture
Every service has its own PostgreSQL database. The whole stack runs on Docker Compose for development, with a Kubernetes (k3d + Helm) path also provided.
Outbox Pattern
When an order is created, the service must both save the order and publish an event. Doing these as two separate operations is the dual-write hazard.
The Outbox Pattern solves it by writing the event record into the same database transaction as the business data:
Application state and the pending event are committed atomically. A publisher then reads and forwards those records to RabbitMQ — the two writes are now safe because the second one is derived from the first.
Saga Pattern
Completing an order touches three services. The Saga pattern coordinates the steps as a sequence of local transactions with compensating actions for earlier steps if a later one fails:
If stock is unavailable or a payment is rejected, compensating messages roll back the earlier steps — the flow was verified with exactly these failure cases.
Technology Stack
| Layer | Technology |
|---|---|
| Services | Order, Stock, Payment — Django + PostgreSQL each |
| Messaging | RabbitMQ with outbox-based publishing (django-outbox-pattern) |
| Gateway | Kong API gateway |
| Runtime | Docker Compose or Kubernetes (k3d + Helm) |
| Demo frontend | Next.js app in web/ |
Key Engineering Decisions
- Service-per-context databases — Order, Stock and Payment own their data; no shared schema dependencies.
- Outbox-based publishing — messages are safe by construction instead of relying on retry luck.
- Saga orchestration over choreography — one coordinator tracks the order lifecycle, which is easier to reason about and test for this flow.
- API gateway in front — a single entry point for clients instead of exposing three service URLs.
- Two deployment targets — Docker Compose for fast local iteration, Kubernetes specs for a more production-like path.
Messaging & API Design
Services exchange domain events over RabbitMQ exchanges and queues. The message flow traces the order lifecycle: an order event published from the Outbox is consumed by Stock (reserve) and Payment (charge), whose outcome events drive the next step.
Entry points in the Docker quick-start:
Database Design
- Order service DB — orders and order items; an outbox table records outgoing events in the same transaction.
- Stock service DB — product inventory; consumes reservations events and publishes outcomes.
- Payment service DB — payment records; consumes payment requests and publishes results.
Each outbox table is the durability anchor for the system: if RabbitMQ is unavailable, publishers retry from the database — nothing is lost.
Authentication & Security
This is a demonstration system, so the security posture is honest and limited: default admin accounts exist for the Django consoles and RabbitMQ (documented as dev only), all traffic in the quick-start is local, and the Kubernetes path is prepared with Helm values per service. API gateway routing and network policy are the natural next hardening steps.
Testing
Verification focused on the behaviour that matters in an event-driven flow — failure handling:
- Created an order and confirmed the happy path through Stock and Payment.
- Simulated unavailable stock — confirmed the order path compensates.
- Simulated rejected payment — confirmed earlier steps are rolled back.
- Used Postman to drive the endpoints and RabbitMQ's console to inspect actual message flow between services.
Challenges & Solutions
- Losing messages on dual-write — solved with the Outbox Pattern: events committed with the data, then published asynchronously.
- Cross-service consistency — the Saga pattern sequences steps and compensates earlier work when a later step fails.
- Privately owned data per service — each service keeps its own PostgreSQL database and only shares messages, keeping coupling low.
- Reproducible environments — Docker Compose startup scripts and a k3d/Helm bootstrap make both runtimes one command.
What I Learned
This project made abstract patterns concrete: I now understand why the Outbox Pattern exists (dual-write is a real hazard, not a theory) and how a Saga keeps a multi-service transaction honest. I also learned to verify distributed behaviour by breaking it on purpose — failing stock and payments in Postman taught me more than the happy path ever did.