Designing Systems That Actually Ship: A Small Template
In my experience, systems that fail to ship usually do so not because they were too hard, but because the design never became a real artifact. The idea stayed in someone’s head or in a long document that did not connect to actual files or testable outcomes. This aligns with Google's Design Docs guide, which argues that a written design document is what forces vague ideas to become concrete decisions.
I use a short system design document that lives inside the repo. It is not architecture theory. It is a few headings that keep the project from drifting.
What the document covers
I keep it short. It answers four questions:
- What problem does the system solve?
- What inputs enter the system?
- What outputs leave the system?
- Where does each piece live in the codebase?
When this exists as docs/system-design.md, the design stops being an idea and becomes an executable plan. Keeping design docs inside the repository (rather than in a separate wiki) is a practice recommended by the GitHub Docs-as-code approach and Write the Docs principles.
With a design doc vs. without one
| Aspect | No design doc | With docs/system-design.md in the repo |
|---|---|---|
| Scope drift | Features added without boundaries | Scope is explicit and bounded |
| File ownership | Files grow with no clear owner | File map assigns each layer to a path |
| Testing clarity | Unclear what "done" means | "Done when" bullets are testable |
| Onboarding | New contributors guess the architecture | Design is readable in one sitting |
| Deployment risk | High — no verification before shipping | Low — design gates before code |
The template I use
# System design## Goal## Problem statement## Inputs## Outputs## Constraints## High-level flow## System boundariesInside the system:Outside the system:## File map- API layer: src/api/- UI layer: src/ui/- Data access: src/data/- Shared utilities: src/lib/- Configuration: src/config/## Data flow1. Input enters through API or UI2. Validation occurs3. Data is processed or stored4. Output is returned to the requester## External dependencies- Database- Authentication provider- File storage- Third-party APIs## Done when- API returns expected responses- UI renders core user flow- Data persists correctly- One end-to-end flow is verified
I fill each section in one or two sentences. If a section needs long paragraphs, the system scope is too large and should be split. The benefits of keeping each section short:
- Forces clarity — if you cannot describe the goal in two sentences, you do not understand it yet.
- Prevents scope creep — long paragraphs usually mean the system is doing too much.
- Stays maintainable — a short doc gets updated; a long doc gets ignored.
- Speeds review — teammates can read and critique the design in under five minutes.
Why this helps
Without a concrete design artifact, projects drift. Features get added without boundaries. Files grow without ownership. Testing becomes unclear. Deployment becomes risky. The 12-Factor App methodology makes a similar argument: explicit configuration and clear boundaries are what keep a system deployable.
A short document does not replace code. It guides code. It is small enough to maintain and strong enough to stop uncontrolled growth.
How I verify it is useful
Before I write code, I check the design against these questions:
- Does
docs/system-design.mdexist and print without errors? - Does every path listed in the file map exist in the repo?
- Are system boundaries filled and non-overlapping?
- Does the data flow map to real folders?
- Can every "done when" bullet be verified with a command, browser check, or test?
If any answer is no, I fix the design first.
Where I use this
This template is not theoretical. I use a version of it in real repos:
- This site's repo includes
AGENTS.mdand structured docs that map to real files - Car-Match includes
docs/with DEPLOYMENT.md, API.md, and TROUBLESHOOTING.md that connect to actual code paths
How the template maps to real repos
| Template section | This site (gatsby-starter-minimal-blog) | Car-Match |
|---|---|---|
| Goal | Portfolio, blog, and local SEO service site | Location-based matching app |
| File map | src/@lekoarts/, src/features/, src/pages/ | src/api/, src/ui/, src/data/ |
| External dependencies | Gatsby 5, Netlify, React 18 | Node.js, MongoDB, Google Maps API |
| Done when | Lighthouse passes, build is clean, NAP is consistent | API returns matches, UI renders flow, data persists |
| Design artifact | AGENTS.md | docs/DEPLOYMENT.md, docs/API.md, docs/TROUBLESHOOTING.md |
What "actually ships" means in practice
A system that ships is not a system with every feature. It is a system where the scope was bounded, the timeline was honest, and the compromises were made on purpose instead of by accident.
In practice, "actually ships" means three things:
- Scope is bounded and written down. The design doc lists what is inside the system and what is outside it. If a feature is not in the boundaries, it does not get built in this iteration. It can go in the next one.
- The timeline reflects the scope. A system with four inputs and two outputs should not take three months. A system with external auth, a database, and a third-party API should not take a weekend. The design doc makes the mismatch visible before you commit to a date.
- Compromises are explicit. You will cut something. The question is whether you cut it on purpose or let it get cut by entropy. A design doc lets you say "we are shipping without caching in v1" instead of discovering at deploy time that nobody built caching.
The systems that did not ship in my experience all failed the same way: the scope was never written down, so it grew silently until the timeline broke, and then the whole thing got shelved. The systems that did ship had a document small enough that someone actually read it before writing code.
Concrete examples: shipped vs. did not ship
I can point at specific projects on both sides.
Shipped — this site (gatsby-starter-minimal-blog). The scope was clear: portfolio, blog, and local SEO service pages. The design doc (in the form of AGENTS.md) maps every feature to a real file path. The constraints were explicit — Gatsby 5, Netlify, no backend. The compromise was no custom CMS; content lives in MDX files in the repo. That shipped and has been live and deploying on every push since.
Shipped — Car-Match. The scope was a location-based matching API with a thin UI. The design doc lived in docs/ as DEPLOYMENT.md, API.md, and TROUBLESHOOTING.md. The constraint was MongoDB as the only data store. The compromise was no real-time updates — matches are computed on request, not pushed. That shipped with a working API and a deployed UI.
Did not ship — an internal dashboard project. I started building a dashboard that was supposed to show "analytics." No design doc. No written scope. Within a week it had auth, three chart libraries, a settings page, and a notification system. None of them were finished. There was no "done when" list, so there was no way to know if it was done. It sat unfinished for months and was eventually abandoned. The failure was not technical. The failure was that the scope was never bounded.
The difference between the shipped projects and the abandoned one was not skill or effort. It was the presence of a short, connected design artifact. The shipped projects had one. The abandoned one did not.
Design decisions and trade-offs I keep making
Every system design involves trade-offs. Writing them down is what separates a decision from a default. Here are the ones I run into most often:
- One data store vs. multiple. One store is simpler to deploy and reason about. Multiple stores fit different access patterns but multiply operational complexity. I default to one unless the design doc can articulate a specific reason for the second.
- Auth built in vs. delegated. Building auth yourself is a trap. Delegating to a provider (Auth0, Clerk, Supabase Auth) costs money or vendor lock-in but removes a class of bugs I do not want to own. I delegate unless the system is a toy.
- SSG vs. SSR vs. SPA. Static generation is the default for content sites because it is cheap, fast, and deployable anywhere. Server rendering is justified when content changes per-request. SPAs are justified when the app is interactive and stateful. I write the choice and the reason in the design doc so it does not get relitigated later.
- Monorepo vs. split repos. For small systems, one repo is easier to navigate and deploy. For systems with independent deploy cycles, splitting reduces coupling. I lean toward one repo until the deploy cadences actually diverge.
These are not universal rules. They are the defaults I write down so that when I deviate, I have to explain why.
Closing
A system design document does not need to be long. It needs to be connected to the codebase. If the design does not point to real files and real verification steps, it is not ready to drive implementation.