QuickBite
A food-ordering system where the whole order workflow runs on events.
The problem
Why it exists
The brief was a system-level QA project, so the CRUD part was never the point. The point was what happens between services. An order is placed, the kitchen accepts it and marks it ready, the customer is notified, and no service ever calls another directly.
That's where the real questions are. What if the broker is down when an order is saved? What if a handler fails halfway? What if the same event arrives twice? QuickBite is built around those questions, and the tests run against real infrastructure to check the answers.
How it works
One order, from click to notification
Step 1 of 8 · Client
Place order
The customer places an order in the React UI. nginx is the only public entry, and it passes /api on to the gateway.
Decisions
What I chose, and what I didn't
One database per service
Each service owns its own Postgres database and never reads or writes another's tables. A service can change its schema without asking anyone, and a slow query in one can't lock another.
Rejected
One shared database. Joins get easier, and every service quietly starts depending on every other service's tables.
A transactional outbox for events
The order and its order.placed event are written in the same transaction, and a poller publishes the event afterwards. If RabbitMQ is down, the event waits in the outbox instead of disappearing.
Rejected
Save, then publish. If the publish fails after the commit, the order exists and nobody ever hears about it.
Retry with backoff, then dead-letter
A failing handler gets three retries, after 1, 2 and 4 seconds, through a dead-letter-based delay queue. After that it lands in a permanent dead-letter queue where someone can inspect it.
Rejected
Requeueing immediately on failure. A message that can never succeed spins in a tight loop forever.
nginx is the only way in
RabbitMQ, Postgres, Redis, the gateway and the frontend are only reachable on the internal Docker network. The whole system exposes exactly one address, on port 80.
Rejected
Publishing every container's port for convenience, and then having to remember to close them all.
By the numbers
What it adds up to
- services behind one gateway, one database each
- 5
- command to run everything: docker compose up
- 1
- retries with backoff before the dead-letter queue
- 3
- infrastructure mocks in the integration and e2e tests
- 0
What I learned
The interesting failures live between services, not inside them. One service on its own is easy to test. The questions that took real thought were about the gaps: the moment after a commit and before a publish, the second delivery of the same event, the broker that's down for thirty seconds.