A small event-driven order pipeline: a Django REST API publishes domain events over Redis pub/sub, a standalone listener reacts to them, and the actual work happens on a Celery task queue — so the API stays fast and every downstream step can be added or changed without touching the others.
POST /api/orders/
│
▼
Django REST API ──creates Order (status=pending)──▶ Postgres
│
│ post_save signal
▼
Redis pub/sub ◀── publish("events:orders", order.created)
│
▼
listen_events (management command, long-running process)
│
│ process_new_order.delay(order_id)
▼
Celery worker ──validates/"charges"──▶ Order.status = confirmed/failed
│
│ publish(order.confirmed)
▼
Redis pub/sub ──▶ listen_events ──▶ send_confirmation_email.delay(...)
Two distinct patterns are deliberately kept separate:
- Pub/sub (Redis) — decouples "something happened" from "who cares".
The API and the Celery tasks never import each other;
listen_eventsis the only place that maps event → reaction, so adding a new reaction toorder.confirmednever touches the API or the payment task. - Task queue (Celery) — does the actual (simulated) work, with retries and a dedicated worker pool, instead of blocking the pub/sub listener.
cp .env.example .env
docker compose up --buildThis starts Postgres, Redis, the Django API (:8000), a Celery worker, and
the event listener as separate containers — mirroring how you'd actually
deploy each piece independently.
Check that the API can actually reach Postgres and Redis:
curl http://localhost:8000/api/health/Create an order:
curl -X POST http://localhost:8000/api/orders/ \
-H "Content-Type: application/json" \
-d '{"customer_email": "a@example.com", "amount_cents": 5000}'Watch docker compose logs -f listener worker to see the event flow from
order.created → payment task → order.confirmed → notification task.
Orders with amount_cents < 100 are treated as a simulated payment decline,
so you can exercise the order.failed path without a real payment gateway.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
cp .env.example .env
python manage.py migrate
python manage.py runserver # terminal 1
celery -A config worker -l info # terminal 2
python manage.py listen_events # terminal 3ruff check orders config
pytest -vCI runs the same commands against real Postgres and Redis service
containers (see .github/workflows/ci.yml) — no mocking the database.
Most demo backends either do everything in the request/response cycle, or
reach for a task queue without a clear story for what decides to enqueue
what. This project's whole point is to keep those two concerns visibly
separate — you can read orders/events.py, orders/tasks.py, and
orders/management/commands/listen_events.py independently and understand
the full system.
MIT