cd ..

Self-Healing Reliability Lab: Trading Platform

Context: Independent, 2026 | Architecture: Docker Compose lab | Role: Design, build, break, fix | Scope: Simulated workload, no real users or money
Docker Compose Python (FastAPI) Apache Kafka (KRaft) MySQL GTID replication HAProxy Runtime API Prometheus + Grafana k6 Fault-injection drills

A trading system is an unforgiving workload for an SRE: stale data costs money, downtime costs more, and an automated action fired at the wrong moment can do more damage than the incident itself. I built this lab to practise failing safely: kill the primary database while orders are flowing, and let the system promote a replica, re-route writes and drain its backlog without a human.

Everything below runs today on Docker Compose: a Python order API, a Kafka event bus, a consumer worker, a MySQL primary/replica pair with GTID replication behind HAProxy, a small incident-control-plane service, and Prometheus with Grafana. Porting this lab to Kubernetes, Terraform and Vault is designed but not built yet; that work lives under Next phase, not mixed in with what runs.

~10 s Failover in lab drills
3 Failed checks before promotion
0 Failed client requests in the load drill
3 Postmortems

Architecture

Writes and reads take different paths, and the API never waits on the database. The control plane watches the primary directly, so it judges the node rather than the route to it.

Ingress and event bus
[ Client / k6 ]
  ──► [ Order API (FastAPI) ]
  ──► [ Kafka topic (KRaft) ]
Processing
[ Consumer worker ]
  ──► [ HAProxy write frontend ]
  retries the connection every 2 s
Data
[ MySQL primary ] ── GTID, async ──►
  [ MySQL replica (read_only) ]
HAProxy writes ──► primary
  (replica held as backup)
HAProxy reads  ──► replica
Control and observability
[ Incident control plane ]
  ── checks every 2 s ──► primary
  3 failures ──► failover script
  ──► HAProxy Runtime API
[ HAProxy exporter · API /metrics ]
  ──► [ Prometheus ] ──► [ Grafana ]
Next phase: designed, not built
Kubernetes (GKE + Proxmox) · Terraform
Vault dynamic credentials
alert rules · CI

Why the API Never Waits for the Database

The order API doesn't write to MySQL. It validates the order, publishes it to a Kafka topic and returns. A consumer worker writes to the primary through HAProxy. That one decision is what makes a database failover invisible to clients: while the primary is down, the API keeps accepting orders and Kafka holds them; the worker retries every two seconds until writes have somewhere to go, then drains the backlog.

GTID Replication, and the Bugs That Taught It

Both MySQL nodes run with GTIDs on (gtid_mode, enforce_gtid_consistency, row-based binlogs, and replica updates logged), and replication uses auto-positioning: the replica asks for every transaction it doesn't have, instead of a binlog file name and offset. That is what makes promotion and failback scriptable. There's no position bookkeeping, and the promoted node already keeps the binlog the old primary needs to catch up from.

What broke on the way

Durability: asynchronous replication and its RPO

Replication is asynchronous. If the primary dies holding transactions it committed but hadn't shipped, the promoted replica won't have them; that window is this lab's RPO. Gating promotion on measured replication lag, so the window is bounded before the system acts, is part of the next phase.

The Incident Control Plane: Automation With Brakes

Fully autonomous remediation is seductive and dangerous: automation that acts on a false positive can do more damage than the original incident. The control plane is deliberately small, so its behaviour is predictable. It checks the primary directly every two seconds with a two-second connect timeout, and it acts on exactly one failure mode.

failover, in order (simplified)
-- on the replica: stop applying
-- the dead primary's stream
STOP REPLICA;
SET GLOBAL read_only = 0;
SET GLOBAL super_read_only = 0;

# HAProxy Runtime API, no reload
disable server <backend>/primary
enable server <backend>/replica
What it doesn't check yet: whether the replica's executed GTID set covers everything the primary acknowledged, and whether replication lag is under a threshold. A promotion should require both, plus confirmation that the replica itself is healthy. Those gates are designed for the next phase; today's guard rails are the three-strike rule, the one-shot latch and human-run failback.

Failure Drill: Kill the Primary Under Load

A fault-injection script force-kills the primary container (docker kill, no graceful shutdown). This timeline comes from the drill's postmortem:

T+0 s: primary killed
The fault-injection script terminates the MySQL primary mid-traffic.
T+1 s: orders keep arriving
A client submits an order; the API publishes it to Kafka and returns success.
T+2 s: the worker waits
The worker pulls the order, finds no database behind HAProxy and starts retrying every two seconds.
T+6 s: failure confirmed
The control plane records its third consecutive failed health check and declares the primary dead.
T+7 s: replica promoted
The failover script stops replication on the replica and makes it writable.
T+8 s: writes re-routed
The script disables the primary and enables the replica for writes through HAProxy's Runtime API.
T+10 s: backlog drained
The worker's next retry lands on the promoted replica; it saves the queued order and drains the backlog.

I also ran the drill in the middle of a k6 spike test. The order API kept answering with success throughout, because it never waits on the database. A provisioned Grafana panel plots HAProxy's per-server UP state for the write backend, so the failover is visible as the primary's line falling while the replica's rises.

SLOs and Error Budget

The lab is designed around one SLO: 99.9% availability for the order API, which leaves an error budget of about 43 minutes a month. The budget is the argument for automating this failover at all. A ~10-second automated recovery spends a sliver of it; the same failure waiting for someone to wake up, diagnose it and promote by hand could spend a large share of the month's budget in one night.

The SLIs are chosen: request success rate, p95 and p99 latency, Kafka consumer lag, replication lag and failover duration. Today Prometheus scrapes HAProxy's built-in exporter and the API's order counter every five seconds. Recording rules, burn-rate alerts and an error-budget dashboard are in the next phase, so I quote no measured availability figures here.

Postmortems

Every serious failure got a written postmortem: what happened, why it happened, and the fix. Three so far:

Next Phase: Designed, Not Built

Engineering Takeaways

The most important decision in this lab wasn't a technology choice; it was deciding where automation stops. The control plane acts on one well-understood failure (a dead primary, confirmed three times), acts once, and leaves failback to a person. The bugs that would have hurt most, a worker that gave up mid-outage and a reload signal HAProxy silently ignored, only surfaced when I broke the system on purpose, which is the point of a lab.

Result: in the drill, killing the primary under load cost about ten seconds of write delay and no failed client requests. The API kept accepting orders, Kafka held them, and the worker drained the backlog as soon as HAProxy pointed writes at the promoted replica.
Hiring for SRE, DevOps or platform roles? I'm happy to walk through this failover design in an interview. Email me, read my CV, or browse all work.