Infrastructure patterns written from scratch in Go, plus a live demo server where each one can be poked at over HTTP. Built as preparation for system design interviews.
The repository runs on a single premise: a pattern is only understood once it has been implemented, tested, and measured. Reading about circuit breakers and writing a circuit breaker whose window actually slides are different levels of understanding. The second one is visible in code; the first one is not.
make test-racemake runThe demo server listens on :8080 and serves a self-describing index at GET / — every endpoint with a note on what it demonstrates and a ready-to-paste curl:
curl -s localhost:8080/ | jq .Watch the circuit breaker trip and then probe its way back:
for i in $(seq 1 10); do curl -s localhost:8080/demo/circuit-breaker | jq -r .circuit_state; donepkg/ the patterns, as libraries — this is the material
cmd/demo-server the bench: each pattern behind an HTTP endpoint with a fake backend
tasks/ warm-up exercises on Go concurrency
assignments/ exercises: spec, acceptance criteria, reflection questions
docs/ planning and self-assessment
Everything in pkg/ is a library with no dependency on the bench, and cmd/demo-server is the only place they are wired together. The split is not cosmetic: a package that cannot be tested without booting a server is not a package.
The server plays both roles — the client defending itself with these patterns, and the unreliable backend it is defending against. That makes failures reproducible without any external dependency.
flowchart LR
curl([curl]) --> TM[traceMiddleware<br/>X-Trace-Id]
TM --> MUX[ServeMux]
MUX --> CB["/demo/circuit-breaker"]
MUX --> RL["/demo/rate-limiter"]
MUX --> RH["/demo/resilient-http"]
MUX --> HG["/demo/hedged"]
MUX --> BF["/demo/bloom-filter/*"]
CB -.->|circuit_breaker| FLAKY["/internal/flaky<br/>fails 60%"]
RH -.->|resilient_http| UNST["/internal/unstable<br/>fails 2 in 3"]
HG -.->|hedged_requests| SLOW["/internal/slow<br/>50–200ms delay"]
RL -.->|rate_limiter| OK[responds immediately]
BF -.->|bloom_filter| OK
Every response carries a trace_id, a measured duration, and a note explaining what just happened and why. The bench is meant to be a debugging tool rather than a demo: one response should be enough to tell whether the pattern fired.
"Working" means: implemented, tests green under -race, behaviour confirmed by a separate check, no known defects.
| Package | What it does | Status |
|---|---|---|
| pkg/circuit_breaker | Breaker with closed/open/half-open states over a sliding bucket window | Working |
| pkg/rate_limiter | Token bucket over a channel, cancellable and stoppable | Working |
| pkg/resilient_http | HTTP client: retries by response class, Retry-After, backoff with full jitter |
Working |
| pkg/hedged_requests | Speculative duplicate request, threshold taken from a Prometheus p90 | Working |
| pkg/bloom_filter | Scalable Bloom filter: bitset, layers, tightening ratio | Working |
| pkg/sharded_map | Sharded map, benchmarked against a single-mutex baseline | Working |
| pkg/transactional_outbox | Transactional outbox: transaction, relay, idempotent consumer | Working |
| pkg/distributed_tracing | Spans with parent/child, propagation over W3C traceparent |
Working |
| pkg/minigin | A hand-rolled HTTP framework | Not started — assignment 06 |
The warm-up exercises in tasks/ — counter, ping-pong, fetcher, timeout, byte-buffer pool — all work and pass -race.
All three lived in this repository under a fully green test suite, which is the reason they are documented rather than quietly fixed. Each one only surfaced on input the tests never supplied.
The sliding window did not slide. The breaker computed a bucket index around a ring but never zeroed a bucket when the ring wrapped back into it, so failures accumulated forever. It was a cumulative counter wearing the name of a sliding window. Two failures, a one-second pause spanning five full windows, then two successes — and the breaker was still open.
The Bloom filter's real false positive rate was 2.6% against a stated 1%. Not a regression; it had been there since the first commit. The cause is arithmetic rather than cryptographic: m from the optimal-size formula is frequently even, the double-hashing step is even about half the time, and an even step over an even modulus keeps the whole progression a + i·b mod m inside one parity class — so the key addresses half the filter. Forcing the step odd brings it to 0.97%.
HedgedDo returned a response whose body was already dead. The shared context was cancelled by defer cancel(), which fires exactly when the caller receives the response and has not yet started reading. Invisible on small responses, because the transport has already buffered those in full — which is precisely why every test passed and the bench appeared to work, since it closed the body without reading it. On a 1 MB streamed response the caller got 16 KB and context canceled.
Full write-ups, including the two hypotheses that turned out to be wrong on the Bloom filter, are in docs/roadmap.md.
Rules, not preferences:
-raceis mandatory.make testexists, but onlymake test-racecounts as green. Concurrent code without the race detector is untested code.- Test behaviour and invariants. "Open connections never exceed
MaxOpenunder 50 goroutines" is a test. "The constructor returned non-nil" is not. - Measure before optimising.
pkg/sharded_mapis benchmarked against a naive mutex map not out of ceremony, but because without a number, "sharding is faster" is indistinguishable from a guess. pkg/miniginuses the standard library only. Calling someone else's router defeats the point of the exercise.- No comments. Code carries its own meaning; the exceptions are a genuinely non-obvious technical detail and
//nolintdirectives, which must always state their reason.revive'sexportedrule is disabled on purpose — it demands a doc comment on every exported identifier, and that conflict belongs in the config, not in a hundred comments. - Reflection is part of the work. Every assignment has a "Мои наблюдения" section. That is the material you actually speak to in an interview; the code is just how you earn it.
make ci runs the full gate — build, vet, test-race, lint — and is currently green end to end. The linter reports zero issues; fifteen findings are suppressed with targeted //nolint, each carrying its reason, because a //nolint without an explanation is just an ignored linter that complains less.
| Target | What it does |
|---|---|
make ci |
The whole gate: build, vet, test-race, lint |
make test-race |
Tests under the race detector — the primary gate |
make cover |
Total statement coverage |
make test-pkg PKG=./pkg/rate_limiter/... |
One package, verbose |
make bench |
All benchmarks, with allocations |
make bench-pkg PKG=./pkg/sharded_map/... |
Benchmarks for one package |
make lint / make lint-fix |
golangci-lint |
make vet |
go vet ./... |
make run |
Demo server on :8080 |
make run-fleet |
Three instances on :8081–:8083 |