Most systems that fail to ship do not fail because of technical difficulty. They fail because the design never becomes actionable. It lives as loose ideas, scattered notes, or long documents that do not point to real files or testable outcomes. This guide fixes that by turning system design into a concrete artifact that lives inside the repo and directly drives implementation.

This is not architecture theory. This is a practical design workflow that results in a real design document, a real file map tied to your codebase, and a real definition of what “done” means. Every step creates a file or a verification step. Nothing here is abstract or hypothetical.

What you are building

You will create a system design document that answers four questions in a way that is directly usable during development.

What problem does the system solve. What inputs enter the system. What outputs leave the system. Where each piece lives in the codebase.

When finished, your repo will contain a single source of truth that connects intent to implementation.

Here is the final state before starting.

ItemPathPurpose
System design documentdocs/system-design.mdDefines system behavior, code locations, and ship conditions

Once this file exists and is filled in, design stops being an idea and becomes an executable plan.

Why this matters

Without a concrete design artifact, projects drift. Features get added without boundaries. Files grow without ownership. Testing becomes unclear. Deployment becomes risky. The system becomes difficult to reason about because no single place explains what the system is supposed to do.

A short, explicit design document prevents this. It does not replace code. It guides code. It is small enough to maintain and strong enough to stop uncontrolled growth.

Prerequisites

You need a repository where the system will live. It can be empty or partially built. If the repo does not have a docs directory, this guide will create it.

No frameworks or tooling assumptions are required. This works for frontend projects, backend services, or full stack applications.

Step 1: Create the design document

The design file starts with structured headings. This prevents vague writing and forces direct answers.

Create the design file with this command:

mkdir -p docs
cat > docs/system-design.md <<'EOF'
# System design
## Goal
## Problem statement
## Inputs
## Outputs
## Constraints
## High-level flow
EOF

Open the file and fill each section with one or two sentences. Keep it brief. If you need long paragraphs, the system scope is too large for a single design document and should be split into phases.

Verify the file exists:

cat docs/system-design.md

If the template prints, the design skeleton is complete.

Step 2: Define system boundaries

Systems fail when boundaries are unclear. This step makes it explicit what is inside the system and what is outside it.

Append a boundary definition section.

cat >> docs/system-design.md <<'EOF'
## System boundaries
Inside the system:
Outside the system:
EOF

Fill these lines. If something belongs in both lists, the boundary is unclear and must be resolved before writing code.

Step 3: Map design to real code paths

A design that does not map to real file paths is not implementable. This step ties abstract behavior to physical code locations.

Append the file map section.

cat >> docs/system-design.md <<'EOF'
## File map
- API layer: src/api/
- UI layer: src/ui/
- Data access: src/data/
- Shared utilities: src/lib/
- Configuration: src/config/
EOF

Now compare these paths with your repo. If your project uses different folder names, update the mapping so each listed path actually exists. Do not move forward until every path listed is real.

Step 4: Define data flow

Data flow explains how information moves through the system. This prevents guesswork during implementation.

Append the data flow section.

cat >> docs/system-design.md <<'EOF'
## Data flow
1. Input enters through API or UI
2. Validation occurs
3. Data is processed or stored
4. Output is returned to the requester
EOF

Replace these steps with your real system flow. Keep it short and numbered. Each step should correspond to code that will exist in one of the mapped folders.

Step 5: Define external dependencies

Systems interact with outside services. This must be declared early to avoid surprises.

Append the dependency section.

cat >> docs/system-design.md <<'EOF'
## External dependencies
- Database
- Authentication provider
- File storage
- Third-party APIs
EOF

Remove any lines that do not apply. Add specific services where known.

Step 6: Define what "done" means

Without a concrete definition of done, systems expand indefinitely. This step defines the smallest shippable unit.

Append the done section.

cat >> docs/system-design.md <<'EOF'
## Done when
- API returns expected responses
- UI renders core user flow
- Data persists correctly
- One end-to-end flow is verified
EOF

Rewrite these bullets so each condition can be verified through a command, browser check, or test. If a condition cannot be tested, it is not a valid done condition.

Step 7: Verify the design is implementable

At this stage, answer these questions.

QuestionVerification
Does the design exist as a file?docs/system-design.md prints successfully
Do all mapped paths exist?Each listed folder exists in the repo
Are boundaries defined?Inside/Outside lists are filled
Is data flow clear?Steps map to code locations
Are done conditions testable?Each has a verification method

If any answer is no, update the design before writing code.

Common failure patterns

FailureCauseCorrection
Design too largeToo many responsibilities in one docSplit into multiple design files
Unclear ownershipFile map missing or inaccurateFix mapping before coding
Endless scope creepNo done definitionRewrite done conditions
Confusing behaviorNo data flow definedAdd data flow steps

These are the most common reasons systems stall before shipping.

Closing

A system design that ships has three properties. It is short. It maps directly to real code. It defines a testable end state.

If your design document meets those conditions, implementation becomes straightforward. If it does not, fix the design before writing code.