Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sys-design

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.

Quick start

make test-race
make run

The 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; done

Layout

pkg/            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 demo server

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
Loading

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.

Packages

"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.

Three defects worth knowing about

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.

House rules

Rules, not preferences:

  1. -race is mandatory. make test exists, but only make test-race counts as green. Concurrent code without the race detector is untested code.
  2. Test behaviour and invariants. "Open connections never exceed MaxOpen under 50 goroutines" is a test. "The constructor returned non-nil" is not.
  3. Measure before optimising. pkg/sharded_map is benchmarked against a naive mutex map not out of ceremony, but because without a number, "sharding is faster" is indistinguishable from a guess.
  4. pkg/minigin uses the standard library only. Calling someone else's router defeats the point of the exercise.
  5. No comments. Code carries its own meaning; the exceptions are a genuinely non-obvious technical detail and //nolint directives, which must always state their reason. revive's exported rule 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.
  6. 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.

Make targets

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

About

Collection of distributed systems patterns and approaches

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages