Skip to content
Back to Tutorials

Getting started with GARDEN

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:

  1. CONTEXT.md (root, one hop) — points it at naming.md and the relevant slice contracts.
  2. shorten/contract.md and resolve/contract.md (one hop from CONTEXT.md) — the agent updates both contracts first: shorten now records a creation time, resolve now checks it and can fail with a new link_expired error.
  3. 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:

For the authoritative statement of each rule you applied here, see the principles reference.