Skip to content

Commit a8493d2

Browse files
committed
Write architecture file
1 parent 22f93b7 commit a8493d2

File tree

1 file changed

+86
-1
lines changed

1 file changed

+86
-1
lines changed

docs/architecture.md

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,86 @@
1-
// docs/architecture.md
1+
# Architecture
2+
3+
## Component Overview
4+
5+
```
6+
┌──────────────────────────────────┐
7+
│ rate-limiter pod │
8+
Client ─ POST──> │ HTTP: 8081 gRPC: 50051 │
9+
│ │ │ │
10+
│ └──────┬─────┘ │
11+
│ │ │
12+
│ limiter.AllowRequest() │
13+
│ │ │
14+
│ store.AllowRequest() │
15+
│ │ │
16+
└──────────────┼───────────────────┘
17+
│ EVALSHA (Lua)
18+
19+
┌──────────────────────────────────┐
20+
│ Redis │
21+
│ sorted set per API key │
22+
│ sliding_window.lua (atomic) │
23+
└──────────────────────────────────┘
24+
```
25+
26+
## Request Lifecycle
27+
28+
1. Client sends `POST \check` with `X-API-KEY: <key>` header.
29+
2. HTTP handler calls `limiter.AllowRequest(ctx, apiKey)`.
30+
3. Limiter looks up the `Policy` for the key (per-key override of default: 100 req/min).
31+
4. Limiter calls `store.AllowRequest(ctx, key, policy)`.
32+
5. Store issues `EVALSHA <sha> rl:<key> <now_ms> <window_ms> <limit>` to Redis.
33+
6. Lua script atomically decides allow (1) or reject (0).
34+
7. Store returns `(bool, error)` to limiter.
35+
8. Metrics recorded: `ratelimiter_requests_total{status=allowed|rejected}
36+
9. HTTP handler writes `200 OK` or `429 Too Many Requests` with JSON body.
37+
38+
## Kubernetes deployment
39+
40+
```
41+
┌───────────────────────────────────────────────────┐
42+
│ Kubernetes cluster │
43+
│ │
44+
│ ┌─────────────────────────────┐ │
45+
│ │ rate-limiter Deployment │ │
46+
│ │ replicas: 3 (HPA: 3–10) │ │
47+
│ │ │ │
48+
│ │ pod-0 pod-1 pod-2 │ │
49+
│ └──────────┬──────────────────┘ │
50+
│ │ ClusterIP Service :8080/:50051 │
51+
│ ▼ │
52+
│ ┌──────────────────────────┐ │
53+
│ │ Redis StatefulSet │ │
54+
│ │ redis-0 (PVC: 1Gi) │ │
55+
│ └──────────────────────────┘ │
56+
│ │
57+
│ Prometheus ─── scrapes /metrics on each pod │
58+
│ Grafana ─── dashboard: deploy/grafana/ │
59+
└───────────────────────────────────────────────────┘
60+
```
61+
62+
## Package dependency graph
63+
64+
```
65+
cmd/server
66+
├── internal/config (load env/yaml, policy map)
67+
├── internal/limiter (AllowRequest — core domain)
68+
│ └── internal/store (Store interface)
69+
│ └── Redis + scripts/lua/
70+
├── internal/metrics (Prometheus counters + histograms)
71+
├── internal/grpc (thin gRPC handler)
72+
└── internal/http (thin HTTP handler)
73+
```
74+
75+
No circular dependencies. `internal/limiter` defines the `Store` interface it depends on (dependency inversion) - this is why `store/store.go` imports `internal/limiter` rather than the other way around.
76+
77+
## Test Strategy
78+
79+
| Layer | Tool | Infrastructure |
80+
|-------|------|----------------|
81+
| Algorithm unit tests | `go test` | None (pure Go) |
82+
| Limiter unit tests | `go test` with `FakeStore` | None |
83+
| Store integration tests | `go test -tags=integration` | Live Redis |
84+
| Concurrency correctness | `go test -tags=integration` | Live Redis + running service |
85+
| Load / latency | k6, vegeta | Live Redis + running service |
86+
| Failure scenarios | Manual (documented in failure_test.go) | k8s cluster |

0 commit comments

Comments
 (0)