build-a-rate-limiter / lesson-01.md
Lesson 01 · The limiter contract and a virtual clock

A virtual clock

Every rate limiter is about time - how many requests in how long - so the first thing we build is time we fully control. A virtual clock starts at zero and only moves when you advance it, which makes every later allow/deny decision exact and reproducible.

The goal

Build a clock that starts at 0 and moves forward only when you advance it.

Start here - the target
TO DO
Scenario: Time moves only when advanced
Givena new clock from NewClock()
WhenNow() is queried
Thenit reports 0
Andafter Advance(5) it reports 5, then after Advance(3) it reports 8, and Advance(0) leaves it at 8
Background

A rate limiter’s whole job is measuring requests against time, so if time is the real wall clock every test becomes a race with sleeps and slop. We dodge that entirely with a virtual clock: time is a plain integer count of ticks that starts at 0 and moves only when you call Advance. Nothing happens in the background. A tick has no fixed real-world duration - it is whatever unit a given limiter treats as its window or refill rate.

Because the clock is injected and hand-advanced, every decision the limiters make later is pinned to an exact moment you chose, so an allow or a deny is never “probably” - it is a value you assert. Advance(0) is a no-op by design: advancing by nothing must not move time. Build this tiny clock now; every limiter in the project reads the current tick from it.

Make it work
// time is a plain int64 count of "ticks" - no real wall clock
type Clock struct {
now int64
}
func NewClock() *Clock { return &Clock{} }
func (c *Clock) Now() int64 { return c.now }
func (c *Clock) Advance(d int64) { c.now += d }
CheckpointDONE
You have a deterministic clock that only moves when told. Commit and stop here.