The Opening Move: Five Components and One CI/CD Pipeline

· tech

#war-story#system-design#django

📑 Contents

With the big picture laid out, and before we get into the battles over comments and stock, let’s open the arsenal we had — because every trade-off in the chapters ahead was made inside the boundaries of this stack. The team was small: 3 backend, 3 frontend, occasionally with 1–2 contractors. The arsenal was plain too: PostgreSQL, Django (API + WebSocket), RabbitMQ, Redis, Celery, all running on GCP. This post answers two questions: why these five, and — why a small team moves fast, which turns out not to be about the stack at all.

Five components, three timescales

These five weren’t thrown together at random. Looking back, they’re exactly one specialist hired per timescale:

Synchronous · ms Django API request in, response out cart · checkout · accounts Real-time · push WebSocket viewer comments → live to the host dashboard Async · sec to min RabbitMQ (the pipe) Celery workers fetch comments · FSM orders · invoices email · order exports PostgreSQL the single truth: orders · stock · members Redis speed: fast banned-user checks One specialist per timescale, with one truth and one speed underneath
How the five divide the work: synchronous goes through Django, real-time is pushed over WebSocket, slow work goes to Celery; PostgreSQL owns the truth, Redis owns the speed.

A few details about that division that the diagram doesn’t carry:

  • The API layer is Django Ninja. It writes almost exactly like FastAPI — type annotations, automatic OpenAPI docs, thin routers — but underneath it’s still full Django: ORM, migrations, admin, all present. It’s FastAPI’s developer experience with Django’s ecosystem dividend, which for a team with three backend engineers pays twice.
  • WebSocket serves the host, not the viewers. Its only job is pushing viewer comments to the host dashboard in real time — the host has to see the chat to keep the show moving. A viewer’s order can lag by seconds (minutes, at peak), but the host’s view has to be live. The system’s whole latency budget is bet on the host’s experience: the host controls the rhythm of the stream, and a host who never runs out of stock and never calls the wrong key generates far fewer complaints. That trade-off comes back in every later chapter.
  • RabbitMQ does exactly one thing from start to finish: it’s Celery‘s pipe. Everything that can be asynchronous is on Celery: fetching comments, FSM batch order placement, invoicing, email, order exports. An external call that’s slow and mustn’t fail, like invoicing, and a loop that runs every two seconds, like comment fetching, were never meant to share a request path.
  • Redis holds only the banned-user list — no sessions. Auth is straight JWT with permissions inside the token, completely stateless, so every API server can verify on its own without consulting shared state. And JWT’s textbook weakness is that once issued you can’t take it back; that Redis blocklist is precisely the revocation mechanism: a comment arrives, we ask Redis whether they’re on the list, and if so we ignore it. Stateless speed plus one centralised veto — we assembled that combination on instinct at the time, and only later found out it’s the industry’s standard answer. What if Redis restarts? Rebuild from the blocklist table in the DB — fast checks in Redis, the fact always in the DB, which is the correct posture for a cache.

One pipeline: where a dozen features a day comes from

The stack was plain, but the pipeline was complete — and that was the real competitive advantage:

push GitLab flow branches GitHub Actions tests + checks gate it staging: auto-deploy push and it ships prod: Cloud Build deploys after approval GCP the whole stack in the cloud The result: six engineers, a dozen features shipped a day staging ships on push · prod has one approval button · CI always stands in front
GitLab flow for branching, CI on GitHub Actions, CD on Cloud Build — three platforms each owning a leg, but all a developer feels is "I pushed and it's live".

The flow is three sentences: after a push, GitHub Actions runs tests and checks; merge into the staging branch and it deploys itself, so anything you want to try is visible the moment you push it; to go to Production, push the prod branch and hit one approval button in Cloud Build. Zero friction to staging, one human gate to Production, CI always standing in front — and that’s how a team of six shipped a dozen features a day.

The conclusion I’ve since drawn: your stack decides what you can build; your CI/CD decides how fast you build it. Anyone can pick boring tech, but give two teams the same five components and one of them will still be white-knuckling a weekly release. The difference was never the components — it’s how many manual steps sit between push and production. Every extra step slows the loop, and speed is a small team’s only advantage.

Reflections

I wouldn’t swap any of them

This is my honest answer after thinking about it for a long time: I’d keep all five of these components if I started over. The biggest reason is Django admin — it’s close to irreplaceable, but its role has to be stated precisely: admin is for engineers only. It’s the engineer’s safe operating console — far safer than running SQL against the DB directly — for setting up Celery schedules and handling every flavour of one-off request; register a model and you have an interface. Operations and support got purpose-built tools instead (this team took pride in good internal tooling; the console chapter goes into it). For a team with 3 backend engineers, admin is a free extra layer of “you won’t fat-finger this”. The second reason is Celery: pair it with RabbitMQ and heartbeat scheduling and you have something close to Airflow‘s capability with none of Airflow’s operational burden. Comment fetching, batch ordering, invoicing, reconciliation jobs all ran on it. Workflow platforms are good things, but they come with their own servers to keep alive and their own potholes — and at a dozen features a day, “nothing to operate” is itself the biggest feature.

Spend the complexity budget where it cuts

A small team’s complexity budget is fixed: whatever you spend on infra, you don’t have for the business. What was smart about these choices (half of it luck, honestly) is that almost the entire budget went to the business — the FSM for comment parsing, the invariants in stock, idempotency in payments. Those are this product’s hard parts. The infra was picked as boring as possible: every component mature enough not to surprise you at 2am, and documented well enough that a contractor could be productive in a week. Confirm the pain first — and the converse holds too: where you haven’t felt pain yet, don’t buy the cure.

The speed was real; the stability was homework we did later

But I have to say the other half out loud: the “fast” in this chapter was bought on credit against “stable”. In the early days the API server ran a single process on a single CPU, got flattened the moment a stream went live, and we scrambled traefik in front with four processes to survive. There was no such role as SRE on that team; all the infrastructure was carried by me as backend lead on the side, watching every single stream by hand with my stomach in knots. CI/CD let us send features to production at speed — but what happened after they landed, we were essentially running blind. I’ll settle that bill in the operations chapter. For now, one line: your opening move decides how fast you run; whether you can keep running is a different subject.