This tutorial builds a tiny link-shortener service from nothing, applying all six
GARDEN principles as you go. It has two capabilities: shorten (turn a long URL into a
short code) and resolve (turn a short code back into the long URL). Follow the steps
in order; each one produces a concrete file and something you can observe. There are no
optional branches here — if you want task-specific guidance afterward, the how-to guides
linked at the end cover that.
What you will end with
A directory tree with one vertical slice per capability, a contract file per slice, a failing-then-passing contract test, a lint rule that enforces a boundary, a short context file, and a simulated agent task that a fresh agent can complete by reading only files within one hop of the edit site.
Step 1 — Start a name registry
Before writing any code, apply Grep-first Discoverability (G) by fixing the canonical names for the concepts this service will use. Pick one name per concept so every later file, test, and error message can reuse it without inventing synonyms.
Create naming.md:
# name registry
- short_code: the generated identifier that maps to a long url
- long_url: the original url a short_code resolves to
- link: the (short_code, long_url) pair, the core domain concept
- shorten: the capability that creates a link from a long_url
- resolve: the capability that looks up a long_url from a short_code
From now on, no file in this tutorial calls short_code a "slug" or "token",
and no file calls resolve a "lookup" or "get". One canonical name per concept.
Step 2 — Scaffold the vertical slices
Organize by capability, not by technical layer, as Atomic Vertical Slices (A)
prescribes. Create one directory per capability, each holding its own entry point,
logic, and tests — not a shared controllers/, services/, models/ split.
link-shortener/
naming.md
shorten/
contract.md
shorten.test
shorten.impl
resolve/
contract.md
resolve.test
resolve.impl
shorten/ and resolve/ do not import each other's internals. Each is small
enough to read in one sitting, and each will hold its own tests rather than a separate
top-level tests/ tree.
Step 3 — Write the contract before the code
Make shorten's contract precise enough that the component could be rewritten from
scratch against it, as Regenerable Components (R) requires. Write it first, before any
implementation exists.
Create shorten/contract.md:
# contract: shorten
## input
- long_url: string, must be a well-formed absolute url
## output
- short_code: string, 6 lowercase alphanumeric characters, unique among stored links
## behavior
- given a long_url not seen before, shorten creates a new link and returns its
short_code
- given a long_url already stored, shorten returns the existing short_code
(idempotent)
- given a malformed long_url, shorten fails with an invalid_long_url error
## errors
- invalid_long_url: long_url is not a well-formed absolute url
This file mentions no implementation detail (no storage engine, no framework).
It is the durable artifact; shorten.impl below is expendable against it.
Step 4 — Write a failing contract test
Check the contract in executable form before any implementation exists: that is
Deterministic Verification (D). Translate the contract into a test that currently fails
because shorten.impl does not exist yet.
Create shorten/shorten.test (pseudocode):
test "shorten returns a 6-character short_code for a new long_url":
result = shorten("https://example.com/a/long/path")
assert length(result.short_code) == 6
assert result.short_code matches "^[a-z0-9]{6}$"
test "shorten is idempotent for a repeated long_url":
first = shorten("https://example.com/a/long/path")
second = shorten("https://example.com/a/long/path")
assert first.short_code == second.short_code
test "shorten rejects a malformed long_url":
assert_raises(invalid_long_url):
shorten("not-a-url")
Observe: running the test suite now reports three failures with a clear
shorten is not defined (or equivalent) error — the gate exists and is red before the
code exists, not after.
Step 5 — Implement against the contract
Write the smallest implementation that turns the three tests green. Keep it inside
shorten/shorten.impl; nothing here reaches into resolve/.
# shorten/shorten.impl
store = {} # long_url -> short_code, private to this slice
function shorten(long_url):
if not is_well_formed_absolute_url(long_url):
raise invalid_long_url(long_url)
if long_url in store:
return { short_code: store[long_url] }
short_code = generate_short_code() # 6 lowercase alphanumeric characters
store[long_url] = short_code
return { short_code: short_code }
Observe: rerunning the test suite turns all three shorten tests green. Repeat steps
3–5 for resolve/ (contract, failing test, implementation) before moving on; the
tutorial omits that repetition here for brevity, but a real pass through GARDEN does
not skip it.
Step 6 — Add a lint boundary rule
Encode the vertical-slice boundary from step 2 as a rule instead of a comment; Deterministic Verification (D) makes lint configuration an executable architecture spec, not convention.
Create lint-rules.md (or your linter's native config, expressed the same way):
# import boundary rule
rule "no-cross-slice-internals":
forbid: shorten/* importing anything from resolve/* except resolve/contract.md
forbid: resolve/* importing anything from shorten/* except shorten/contract.md
If a later change makes resolve.impl reach directly into shorten's private
store, the lint gate fails on that change — the boundary is enforced mechanically, not
by someone remembering to review for it.
Step 7 — Write the root context file
Keep a short, hand-written context file rather than an autogenerated dump, following Navigable Knowledge (N). Keep it under roughly 200 lines and load-bearing only.
Create link-shortener/CONTEXT.md:
# link-shortener context
- canonical names: see naming.md
- one vertical slice per capability: shorten/, resolve/
- every slice has a contract.md; read it before editing that slice's impl
- import boundary: a slice may depend on another slice's contract.md, never its impl
- tests are colocated with the slice they verify; a green test suite is required
before any change is considered done
This file states only what is not already obvious from opening the directory
tree — it does not restate the contracts themselves, and it links to naming.md and
each slice's contract.md rather than duplicating their content.
Step 8 — A simulated agent task: add expiry
Now simulate handing this codebase to a fresh agent with a one-sentence task: "links should expire 30 days after creation; resolving an expired short_code should fail."
Follow what that agent needs to read, in hop order:
CONTEXT.md(root, one hop) — points it atnaming.mdand the relevant slice contracts.shorten/contract.mdandresolve/contract.md(one hop fromCONTEXT.md) — the agent updates both contracts first:shortennow records a creation time,resolvenow checks it and can fail with a newlink_expirederror.shorten/shorten.test,resolve/resolve.test,shorten/shorten.impl,resolve/resolve.impl(one hop from each contract) — the agent adds a failing test per updated contract, then updates each implementation to turn it green.
The agent never had to open a directory outside link-shortener/, never had to
guess a name (the registry already defines link, short_code, resolve), and the
lint boundary rule from step 6 did not need to change, because expiry is internal to
each slice's own contract and implementation. This is a one-context task: its requisite
context — CONTEXT.md, two contracts, two tests, two implementations — fits comfortably
in a single agent context window.
Where to go next
This walkthrough has covered all six GARDEN principles.
For task-specific guidance beyond this walkthrough, see:
- Apply GARDEN to a new project
- Retrofit a legacy codebase
- Set up verification gates
- Review code as an agent
For the authoritative statement of each rule you applied here, see the principles reference.