Skip to content

Technical Design

You’re about to write docs/design.md for a system that barely exists yet, or you’re a few sprints in and every integration turns up a mismatch nobody planned for. This page covers what a design document has to decide (the architecture, the interfaces, the data), how to test the risky parts with a spike before you commit, how to check a design with someone other than its authors, and how to keep the document true while the code moves. It doesn’t cover recording individual decisions in Architecture Decision Records or configuring your AI tools.

Technical design is deciding how the system will be built, before or alongside building it, between the requirements (what the system must do) and the implementation. The SWEBOK Guide treats architecture and design as separate knowledge areas. The part a small team needs written down answers three questions:

  1. What are the major components, and how do they interact? (Architecture)
  2. What are the interfaces and contracts between them? (Interface design)
  3. What are the riskiest assumptions, and how will you test them? (Risk and prototyping)

The answers go into a design document: a written artifact that captures the structure and the diagrams. Write only what a new contributor needs to change the system without breaking it, because every section you add is one more thing someone has to keep true.

The design document holds the big picture. Individual decisions along the way (PostgreSQL over MongoDB, a monolith instead of microservices, one data pipeline shape over another) go into Architecture Decision Records (ADRs). The design document shows what the system looks like; the ADRs explain why it looks that way and what else was on the table.

Technical design works at several levels of abstraction. Think through at least the system level and the interfaces, because those are the two a teammate needs before writing code that has to fit with someone else’s.

The highest-level view names the major subsystems, services, or modules and how they communicate. Simon Brown’s C4 model structures that view as four levels of zoom:

  • Context diagram (C4 level 1): your system as a black box, showing who and what interacts with it (users, external services, data sources, hardware). C4 pitches it at everybody, technical or not, which makes it the diagram for your project partner.
  • Container diagram (C4 level 2): the applications and data stores that must be running for the system to work, and how they communicate. For a web application, these might be a frontend, an API server, and a database. For a data pipeline, an ingestion service, a processing framework, and a storage layer. For an embedded project, firmware modules and a host application.
  • Component diagram (C4 level 3): the internal structure of a single container, for the developers working inside it.
  • Code diagram (C4 level 4): the implementation details of one component, usually a UML class diagram or an entity-relationship diagram. Generate it from the code or the schema when you need it, because a hand-drawn copy goes stale as soon as the code changes.

Draw the context diagram and one level below it, and stop there unless a reader has a question only a deeper level answers. C4’s own documentation says those two are enough for most teams. C4 doesn’t prescribe a notation, so draw the boxes in whatever tool your team already uses; what it does ask is that every diagram say which level of zoom it’s at, so nobody argues about a component while looking at a container.

Draw the first version together, because a diagram surfaces disagreements that words hide. Four people can agree that “the API talks to the database” while holding four different pictures of what that means, and the sentence won’t show the difference. A boxes-and-arrows sketch will, as soon as someone points at a box and says that’s not where that lives.

Philippe Kruchten’s 4+1 View Model predates C4 and answers a different question: not how far to zoom in, but whose concerns a view addresses. It splits the description into five concurrent views:

  1. Logical view: the object model of the design, which serves the functionality end users see.
  2. Process view: the concurrency and synchronization aspects of the design.
  3. Development view: the static organization of the software in its development environment, as modules and subsystems arranged in layers.
  4. Physical view: the mapping of the software onto the hardware, which reflects how the system is distributed.
  5. Scenarios (the +1): a few selected use cases traced through the other four views, which illustrate the design and check that the views agree.

Kruchten gave each view its own notation, mostly derived from Booch’s. If you draw them in UML today, the closest equivalents are class diagrams for the logical view, sequence or activity diagrams for the process view, package diagrams for the development view, and deployment diagrams for the physical view.

Use 4+1 when your readers have distinct concerns (a partner who cares about deployment, a maintainer who cares about module boundaries), and C4 when the question is depth. Pick one, because two sets of diagrams describing the same system double the upkeep and start disagreeing with each other.

Wherever two components meet, there’s an interface: a contract that defines what each side provides and expects. Write these contracts down early, because an agreed contract lets two teammates build both sides in parallel, before either implementation exists.

What “interface” means depends on the project:

  • In a web or mobile application, the interfaces are the API endpoints (paths, methods, request and response schemas, error codes) and the database schemas (tables, relationships, constraints).
  • In a data pipeline or research project, they’re the stage boundaries: the format one stage outputs and the next one expects. A CSV with specific columns, a serialized model artifact, a set of image files in a particular directory structure.
  • In a library or framework contribution, the interface is the public API: which functions, classes, or modules are exposed, and what their signatures and guarantees are.
  • In any project with asynchronous communication, the interfaces include the event or message formats: what gets published, what gets consumed, and what happens when a message is malformed or late.

The contract doesn’t need to be elaborate. A Markdown table, an OpenAPI specification, or a well-commented type definition file can each serve. Get the team to agree on it before anyone builds toward the boundary from both sides, because a mismatch found on paper costs an edit and one found at integration costs a rewrite on one side.

Sketch your data model early, even informally, because it’s one of the most expensive decisions to change late. A late change means database migrations and rewritten queries in an application, or reformatted datasets and invalidated results in a research project.

Start with three questions:

  • What are the core entities or data objects, and how do they relate to each other?
  • What data must be persisted, and what can be derived at runtime or recomputed on demand?
  • What data has privacy, compliance, or non-disclosure agreement (NDA) implications?

For projects with a database, the answers become entity-relationship diagrams, table schemas, and an indexing strategy. For research or data-intensive projects, the equivalent questions are where the data comes from, how it’s preprocessed, what format it’s stored in, and how someone else would reproduce the pipeline from raw input to final output. Version your datasets, or at least record each one’s source, retrieval date, and preprocessing steps, because a result nobody can regenerate from its inputs can’t be checked.

Sometimes the right move is to build a small, throwaway prototype that answers one question before you commit to a design: Does this third-party API do what its documentation claims? Is this library fast enough for your use case? Does this algorithm generalize to real data?

Extreme Programming calls this a spike: a very simple program that explores possible solutions to one technical problem, ignores every other concern, and is expected to be thrown away. Its goal is to reduce the risk of that problem, and its output is a finding, not production code. Record the finding in an ADR and delete the code.

Time-box every spike. Extreme Programming suggests a pair of developers for a week or two; on a small team with other work in flight, two or three days is usually what you can afford. The time box matters because a spike without an end date quietly becomes the first draft of production code, without the review and tests production code gets.

Spike the riskiest assumption first. If the design depends on a component or an integration you’ve never built, a spike finds out whether it works while the design can still change around the answer.

Write the design document as an explanation of how the system works (or will work) for someone who hasn’t read the code. It’s tempting to treat it as a template to fill in, section by section, but a filled-in template answers the template’s questions. A useful design document answers the questions your team and your project partner actually have, briefly enough that people read it more than once.

The docs/design.md below is for a shift board: a web app where a nonprofit’s volunteer coordinators post shifts and volunteers claim and release them. A team of three builds it, Priya among them. The document links an ADR for every significant “why” instead of arguing it inline. A research project swaps in a workflow diagram, the pipeline’s stage formats, and a reproducibility plan; a contribution to an open source project describes the existing architecture and where your change fits into it.

# Design
## Overview
Shift board for <partner> (requirements: docs/requirements.md): a React
front end, a Node API, and Postgres on Render, behind Google Workspace SSO.
## Architecture
![C4 context diagram](./diagrams/context.png)
![C4 container diagram](./diagrams/containers.png)
- Web app (React): the board, the claim and release flows.
- API (Node, Express): auth check, shift rules, notifications.
- Postgres, managed by Render: shifts, claims, users.
- Email (Postmark), called from the API on release.
## Interfaces
- REST, JSON (ADR-002). Spec in docs/api/openapi.yaml; the client is
generated from it. POST /shifts/{id}/claims returns 409 when the shift is full.
## Data
- Postgres through Prisma (ADR-001): User, Shift, Claim (ERD in
./diagrams/erd.png). One Claim per (shift, user), enforced by a
database constraint.
## Deployment
- Render web service plus managed Postgres; a preview deploy per PR.
- Production: the partner's Render account, owned by their IT lead.
Their security sign-off is R-2 in the risk register, docs/risks.md.
ADR-004.
- Rollback: redeploy the previous build (tried once, works).
## Open questions
- SSO on the partner's Workspace tenant is unproven; Priya spikes it first.
- Can Postmark send from the partner's domain without DNS changes on
their side? Spike it before building the release emails.
## Deferred decisions
- Notification delivery: email from the API for now; a queue only if
volume calls for one, recorded in an ADR when it does.

If you want a template rather than a blank page, arc42 is the established one: twelve sections covering goals, constraints, context, solution strategy, building blocks, runtime, deployment, cross-cutting concepts, decisions, quality, risks, and a glossary. It prescribes structure, not notation, so C4 or UML diagrams drop into it unchanged. Keep the sections you need and delete the rest, because a small system described in all twelve is mostly empty headings a reader has to skip.

The Unified Modeling Language (UML) gives you a standard vocabulary for software structure and behavior. Pick the diagrams that answer a question your team is uncertain about right now, and skip the rest:

  • Sequence diagram: how components interact over time for one scenario. Draw one for your authentication flow or your main data flow, because that’s where the most components hand work to each other.
  • State diagram: the lifecycle of an entity with distinct states (an order, a job, a claim).
  • Class or entity-relationship diagram: static structure.
  • Deployment diagram: what runs where, which the deployment section of your design document already needs.

Activity, component, package, and composite-structure diagrams exist too; the UML diagram reference covers all of them.

The professionals in one interview study used far less of UML than the full diagram catalog implies. Petre (2013) interviewed 50 professional software engineers from 50 companies: 35 didn’t use UML in their current work, and 11 used it selectively and informally, keeping a diagram only as long as it was useful and then discarding it (Section IV). The engineers who didn’t use it had considered it, often adopting it for a project or two, and found no advantage over what they already did (Section V). The selective users reached most often for class, sequence, and activity diagrams, and reported using sequence diagrams to elicit requirements and behaviors (Table II).

Check the design with someone other than its authors before you build on it, because the people who wrote it share its blind spots. Run three checks:

  • Review it with your project partner: does the architecture support their use cases and their deployment constraints?
  • Walk one key scenario end to end: trace a representative workflow (a user action, a data processing run, an experimental trial) through every component in your diagram, and note each place where the diagram doesn’t say what happens next.
  • Ask what happens when each component fails, and whether the rest of the system degrades or stops.

A peer technical design review puts your design in front of another team, who ask whether it’s fit for purpose, whether it can grow, and whether it solves the right problem.

The design is working when the problems it was supposed to prevent stop happening:

  • Integration works the first time: two components built separately meet and fit. If the sprint before a demo goes to reconciling interfaces, the interface section wasn’t specific enough.
  • Onboarding is fast: a new teammate, a reviewer, or an AI tool with the document in context can find its way around the code without a walkthrough.
  • Decisions stick: an approach the team rejected doesn’t get proposed again, by a person or a tool, because an ADR says why it was rejected.
  • The diagram is still true: when the architecture drawing and the code disagree, the document has stopped being read.

Your design will change, and that’s expected. What you want is for each change to be deliberate and recorded, rather than the sum of local decisions teammates made independently. When the design changes significantly, update docs/design.md in the same pull request as the code, and write an ADR that records the change, the alternatives, and the reason.

Decisions made at a whiteboard are easy to lose. Cherubini et al. (2007) interviewed developers at Microsoft and checked what they heard with a survey of 427 responses: most diagrams were transient, and diagrams that documented design decisions were often drawn in those temporary sketches and then lost (Abstract; pp. 559, 563). When a whiteboard session settles something, photograph the board and write the outcome into the design document or an ADR that day, while the people in the room still agree on what was decided.

AI coding tools have changed how individuals write software without removing the need for technical design; on a team, they raise it. Working alone, a design document can feel unnecessary: the tool reads the codebase, infers the patterns, and generates code that fits, so the codebase acts as the design.

Three or four people, each with their own tool, is a different problem. Every assistant works in its own local context: the files it was shown, the conversation it had, the instructions it was given. It doesn’t know what a teammate’s tool suggested yesterday, what the team agreed in a meeting, or which approach was considered and rejected. Each person-and-tool pair can drift toward its own reading of how the system should work, and the codebase grows fast and incoherent: one member’s tool adds an abstraction layer where another’s used a utility function, names diverge, error handling multiplies. It compiles and the tests pass, so the inconsistency is often embedded by the time anyone notices it, and a tool repeats an inconsistent pattern as quickly as a good one.

Against that drift, give every teammate and every tool the same design document: one account of how the system is structured and which boundaries to respect. Two practices put it to work:

  • Give the design to your AI tools the way you’d brief a new teammate: point your AGENTS.md at docs/design.md, and copy in only the rules a tool must never break (boundaries, naming, interface contracts), so every suggestion starts from your architecture. A copy of the whole design drifts from the original. AI Project Setup has the files and what belongs in them.
  • Use ADRs to record what was rejected, not only what was chosen. An AI tool has no memory of your team’s discussions, so without an ADR recording that microservices were considered and rejected for their deployment complexity, it may suggest exactly that approach again.

Review is where drift gets caught, and the design document is the reference that review checks generated code against. How the team reviews AI-generated code, and how loosely it treats spikes and prototypes, is team process, which the working agreement covers.

Best Practices for Writing a Design Document

Section titled “Best Practices for Writing a Design Document”
  • Split any box in a diagram that you can’t describe in one sentence, because teammates will each read an overloaded box differently.
  • Make implicit dependencies explicit. If service A only works when service B is running, say so, because the unwritten dependency is the one that breaks a deploy.
  • Treat interface definitions as more stable than implementations. Change an implementation freely; change a contract only with the people on both sides, because a contract change breaks code you didn’t write.
  • Include the design decisions you deferred on purpose, so a reader can tell a deliberate gap from an oversight. Product scope and non-goals belong in the requirements.
  • Write for the next person joining the project with no context, who can’t tell which unstated assumptions the rest of the team shares.

No design survives contact with the implementation. The document will be wrong in places, and much of its value is in what the team worked out while writing it. So a design document written by one person the night before a review has delivered almost none of that value, however polished it looks, because the other teammates never had the argument that would have changed their picture of the system. Cherubini et al. (2007) found something similar about whiteboard sketches: the value of a transient sketch was secondary to the setting it was drawn in, and it dropped as soon as that setting ended (p. 563).

Over-design looks like diligence, which makes it easy to wave through a review. It has a recognizable shape: a plugin architecture for the one plugin that exists, an abstraction layer over a database you’ll never swap, microservices for a system four people are building. Fowler’s YAGNI argues that a feature built for a presumed future need costs effort now and complexity every later change has to carry, and costs you even when the need arrives, because it delayed the work that mattered now. His MonolithFirst makes the same argument against starting a new system as microservices. Build for the load and the team you have, and add the flexibility when a real second case shows up.

Fowler’s Is Design Dead? contrasts planned design, which settles the big decisions before coding, with evolutionary design, which grows as the system is built. He argues that evolutionary design depends on practices like automated tests, continuous integration, and refactoring that keep the code cheap to change, and that those practices strike a new balance between up-front design and refactoring rather than doing away with design. A project with a fixed end date and an external partner can’t fully afford either style, so this guide takes a narrow position: decide in advance only what’s hard to reverse (the data model, authorization boundaries, deployment topology, integration contracts), and let the rest emerge. Sort decisions by reversibility rather than importance, because an important decision that’s easy to reverse can wait until you know more, and a hard-to-reverse one only gets more expensive with every line built on it.

The diagrams in your design document will go out of date. Cherubini et al. (2007) found diagrams in documentation were often out of date, because keeping them in sync with the code was costly (p. 564). That’s the case for drawing few of them: a context diagram and a container diagram change only when the architecture does, while a class diagram changes every time someone adds a field. Treat a hand-drawn diagram that nobody has updated in months as a question for the team.

Your architecture will tend to mirror your team’s communication structure, whether you plan it or not. Mel Conway made that argument in “How Do Committees Invent?”, and it’s now known as Conway’s law. MacCormack et al. (2012) tested a version of it, the mirroring hypothesis, on five matched pairs of software products of similar function and size, each pairing a product from a tightly coupled organization (usually a commercial firm) with one from a loosely coupled open source community: in every pair, the product from the loosely coupled organization was significantly more modular (Abstract; Table 4). On a four-person team where two people rarely talk, expect a seam between their parts of the code, whether or not the design document put one there. If the design needs those two people to share a module, either change who works on it or write the interface between them down first.

In industry, design documents go by many names: tech spec, RFC (request for comments), design doc, TDD (technical design document). Malte Ubl’s account of design docs at Google describes them as relatively informal documents written before coding starts, covering the context and scope, the goals and non-goals, the design, the alternatives considered, and cross-cutting concerns such as security and privacy. Open source projects formalize the same idea: PEP 1 defines a Python Enhancement Proposal as a design document, and the Rust RFC process asks for a written proposal before any substantial change to Rust, Cargo, or crates.io.

Amazon’s Working Backwards process, as Werner Vogels describes it, writes the launch documents first, the press release and the FAQ, then works toward documents nearer the implementation.

In research, the closest equivalent is the methods section of a paper, which describes the approach in enough detail for someone else to replicate it. For computational research, that includes the architectural and algorithmic decisions needed to reproduce the results.

Architecture writing skews toward systems far larger than yours, so these are picked for a small team with a fixed deadline.

Sources and further reading

Activities that exercise this