Think Before You Code: Writing Design Docs That Drive Decisions

You get a new ticket. It's a medium-sized feature — not trivial, not a rewrite, but enough moving parts that you need to think about it. What do you do?
If you're like most developers, you open your editor and start coding. Maybe you sketch some ideas in your head. Maybe you chat with a colleague for five minutes. Then you write code for two days, open a PR, and discover during review that:
- You missed a critical edge case that changes the entire approach
- Another team already built something similar with a different interface
- The tech lead had a strong opinion about the data model — but nobody asked them
- Your solution works, but creates a coupling that will hurt in three months
Two days wasted. Not because the code was bad — but because the thinking happened too late.
This is the single most common gap between mid-level and senior developers. Mid-level developers jump to code. Senior developers think first, then code — and they make that thinking visible through design documents.
What You'll Learn
✅ Why design docs matter more than you think
✅ When to write one (and when not to)
✅ A practical design doc template you can use tomorrow
✅ How to write the most important section: trade-offs
✅ Real examples: API versioning, notification system, caching layer
✅ How to use design docs to influence decisions and build trust
✅ Common mistakes that make design docs useless
Why Design Docs Matter
A design doc is not bureaucracy. It's not a formality. It's a thinking tool.
Writing forces clarity. When the design lives only in your head, it feels complete. But the moment you try to write it down — the gaps appear. The edge cases you glossed over. The assumptions you didn't question. The alternatives you didn't consider.
What a design doc actually does
-
Forces you to think before you build. Writing is thinking. You can't write a clear design doc without understanding the problem deeply.
-
Surfaces disagreements early. A PR review is the worst place to discover that your approach conflicts with the team's direction. A design doc surfaces these conflicts before any code is written.
-
Creates a shared mental model. When the team reads your doc, everyone understands not just what you're building, but why you chose this approach over the alternatives.
-
Documents decisions for future developers. Six months from now, someone will ask "why did we build it this way?" The design doc is the answer — not a stale comment in the code, but a complete record of context, constraints, and reasoning.
-
Builds your reputation as a thoughtful engineer. The developers who write design docs are the ones who get trusted with bigger problems. Not because of the document itself, but because the document proves they think before they act.
The goal of a design doc isn't to produce a document. It's to produce clarity.
When to Write a Design Doc
Not everything needs a design doc. Writing one for a bug fix or a simple CRUD endpoint is overkill. But skipping one for a significant feature is how you end up rewriting code two weeks later.
Write a design doc when:
- Multiple valid approaches exist. If there's clearly only one way to do it, just do it. If you're debating between two or three approaches — write it down.
- The change crosses team boundaries. If your work affects another team's API, data model, or deployment — document your approach so they can weigh in before you start.
- The change is hard to reverse. Database schema changes, public API contracts, infrastructure decisions — these are expensive to undo. Think twice, write once.
- You're not sure about the approach. If you have doubt, that's a signal. Writing a design doc is how you resolve doubt before it becomes wasted code.
- The feature will take more than 2-3 days. If you're investing significant time, spending 1-2 hours on a design doc is cheap insurance.
Skip a design doc when:
- The solution is obvious and well-understood by the team
- It's a bug fix with a clear root cause
- The change is small and easily reversible
- You're prototyping or exploring (write the doc after you learn something)
The Design Doc Template
Here's a practical template. It's intentionally short — a design doc should take 1-2 hours to write, not a week. Aim for 1-3 pages, not 10.
1. Problem Statement (3-5 sentences)
What problem are you solving? Why does it matter now? What happens if we do nothing?
Bad example:
"We need to add notifications to the system."
Good example:
"Users currently have no way to know when their order status changes unless they manually refresh the page. Support tickets about 'missing orders' have increased 40% in the last quarter. We need a notification system that proactively informs users of status changes across web and email channels."
The difference? The good version tells you why this matters, how urgent it is, and what scope you're dealing with.
2. Goals and Non-Goals
Explicitly state what you're trying to achieve — and what you're NOT trying to achieve. Non-goals are just as important as goals because they prevent scope creep.
Example:
Goals:
- Notify users when order status changes (placed → confirmed → shipped → delivered)
- Support web push notifications and email
- Users can configure which notifications they want to receive
Non-goals (for this iteration):
- SMS notifications (future phase)
- Real-time in-app notification center (separate project)
- Notification analytics or A/B testing
- Notifications for admin/internal users
3. Proposed Solution
Describe your approach at the right level of detail. This is not a code review — you don't need to specify every function signature. But you do need to explain:
- The high-level architecture
- Key data models or schema changes
- API contracts (if other teams consume them)
- How it integrates with existing systems
Use diagrams. A picture of how data flows through your system is worth a thousand words of prose.
4. Alternatives Considered (The Most Important Section)
This is where senior thinking shows. Anyone can propose a solution. The senior move is showing what you didn't choose and why.
For each alternative:
- Describe the approach in 2-3 sentences
- List the pros
- List the cons
- Explain why you didn't choose it
5. Trade-offs and Risks
Every design has trade-offs. Name them explicitly. This shows maturity and builds trust with reviewers.
6. Implementation Plan
Break the work into phases or milestones. This isn't a detailed sprint plan — it's a rough sequence that shows you've thought about delivery order.
7. Open Questions
Things you don't know yet. Things you need input on. This section invites collaboration and shows intellectual honesty.
How to Write the Trade-offs Section (The Skill That Sets Seniors Apart)
The trade-offs section is where most design docs either shine or fall flat. Here's how to do it well.
The comparison table
Always use a table. It forces structure and makes the comparison scannable.
Example: Choosing a notification delivery strategy
| Criteria | Option A: Synchronous (in-process) | Option B: Async via message queue | Option C: Third-party service (e.g., Novu) |
|---|---|---|---|
| Complexity | Low — just call the email/push API inline | Medium — need message broker setup | Low — SDK integration |
| Reliability | Poor — if email fails, order update fails | High — retry with dead-letter queue | High — managed by vendor |
| Latency impact | Bad — adds 200-500ms to order API | None — fire-and-forget | None — fire-and-forget |
| Cost | Free (infrastructure already exists) | Low (managed queue service) | $$ at scale (per-notification pricing) |
| Vendor lock-in | None | None | High |
| Team familiarity | High | Medium (we use RabbitMQ elsewhere) | Low (new vendor) |
| Observability | Hard to debug failures | Good (queue metrics, DLQ) | Depends on vendor dashboard |
The decision and reasoning
After the table, write your recommendation with reasoning:
Recommendation: Option B (Async via message queue)
Option A is a non-starter — coupling notification delivery to the order API creates a fragile system where an email provider outage blocks order processing. Option C would save development time but introduces vendor lock-in and recurring costs that don't justify the value at our current scale (~5K notifications/day).
Option B gives us reliability through retries and dead-letter queues, zero latency impact on the main API, and uses infrastructure patterns our team already knows from the payment processing pipeline. The setup cost (1-2 days for queue configuration) is a one-time investment.
What makes this effective?
- You didn't just pick your favorite. You showed three real options.
- You evaluated on multiple criteria. Not just "which is easiest" but complexity, reliability, cost, team familiarity.
- You explained the rejection reasoning. "Option A is a non-starter because..." tells the reader you thought deeply.
- You connected to team context. "...uses infrastructure patterns our team already knows" shows you're not designing in a vacuum.
Real Example: API Versioning Design Doc
Let's walk through a complete, condensed design doc for a real problem.
Problem Statement
Our public REST API serves 200+ external clients. We need to make breaking changes to the /users endpoint (restructuring the response format to support multi-tenancy). Currently, there's no versioning strategy — any breaking change would break all clients simultaneously. We need a versioning approach that allows us to evolve the API without disrupting existing integrations.
Goals
- Introduce API versioning for all public endpoints
- Allow breaking changes without disrupting existing clients
- Provide a clear deprecation and migration path
Non-Goals
- Internal API versioning (microservice-to-microservice)
- Automated client migration tools
- GraphQL migration (separate initiative)
Alternatives Considered
| Criteria | URL Path (/v1/users) | Header (Accept-Version: 1) | Query Param (?version=1) |
|---|---|---|---|
| Discoverability | Excellent — visible in URL | Poor — hidden in headers | Moderate — visible but cluttered |
| Caching | Easy — URL-based cache keys | Hard — need Vary header | Moderate — query string in cache key |
| Client effort | Low — just change base URL | Medium — modify request headers | Low — add parameter |
| Routing | Simple — path-based routing | Complex — header inspection | Moderate — query parsing |
| Industry adoption | Very common (Stripe, GitHub) | Less common (some REST purists) | Rare for versioning |
| Proxy/CDN friendly | Yes | Depends on configuration | Yes |
Recommendation: URL Path Versioning
URL path versioning (/v1/users, /v2/users) is the most practical choice for our situation:
- Our clients are mostly server-side integrations that configure a base URL once — changing
/v1/to/v2/is trivial. - Our CDN caches aggressively by URL — header-based versioning would require Vary header configuration across all endpoints, which we've had issues with before.
- Industry precedent matters: Stripe, GitHub, and Twilio all use URL path versioning. Our clients expect it.
Trade-offs we accept
- URL "pollution": The version is visible in every URL. We accept this because discoverability outweighs aesthetics.
- Route duplication: We'll need to maintain two route handlers during the transition period (3-6 months). We mitigate this with a shared service layer — only the controller/serializer changes between versions.
- No "content negotiation" purity: REST purists prefer header-based versioning. We prioritize pragmatism over theoretical correctness.
Implementation Plan
- Week 1: Add version prefix routing (
/v1/*maps to existing handlers) - Week 2: Build
/v2/usersendpoint with new response format - Week 3: Update API documentation, notify clients of
/v2availability - Month 2-4: Migration period — both versions active
- Month 5: Deprecation warning on
/v1responses - Month 6: Sunset
/v1
Open Questions
- Do we version the entire API at once, or allow per-endpoint versioning?
- What's our policy for how long deprecated versions stay active?
- Should we add a
Sunsetheader to deprecated endpoints (IETF draft standard)?
Real Example: Caching Layer Design Doc (Condensed)
Problem Statement
Product catalog API response times have degraded from 50ms to 350ms p95 over the last quarter as the catalog grew from 10K to 100K items. The database is the bottleneck — each request runs 3-4 JOINs across the products, categories, and pricing tables.
Options
| Criteria | Application cache (in-memory) | Redis cache | Database materialized view |
|---|---|---|---|
| Latency reduction | ~1ms (same process) | ~5ms (network hop) | ~50ms (still DB, but pre-joined) |
| Cache invalidation | Hard (per-instance, no sharing) | Manageable (central, TTL + event-based) | Automatic (refresh on schedule) |
| Memory cost | Expensive (each instance holds full cache) | Moderate (shared, single copy) | Free (DB handles it) |
| Horizontal scaling | Bad (cache per instance = inconsistency) | Good (shared cache) | Good (DB handles it) |
| Complexity | Low | Medium | Low |
| Team experience | High (used in auth) | High (used in session management) | Low (never used in this project) |
Recommendation: Redis cache with event-based invalidation
In-memory cache doesn't work for our setup — we run 4 API instances behind a load balancer, and cache inconsistency between instances would cause users to see different prices depending on which instance handles their request.
Materialized views would solve the query performance issue but don't address repeated identical queries — we'd still hit the database for every request. At 500 req/s, that's unnecessary load.
Redis gives us sub-5ms reads for repeated queries, shared state across all instances, and we already have Redis infrastructure from our session management setup. We'll invalidate on product/pricing update events from the admin service, with a 5-minute TTL as a safety net.
Trade-offs
- Stale data window: Up to 5 minutes for non-event-driven changes (acceptable for catalog data, not for inventory counts — those bypass cache)
- Additional infrastructure dependency: Redis becomes a critical path component. Mitigation: fallback to database on Redis failure (circuit breaker pattern)
- Cache warming on deployment: First requests after deploy will be slow. Mitigation: pre-warm cache during deployment rollout
Common Mistakes That Kill Design Docs
1. Writing a novel
A design doc is not a specification. If it's longer than 3 pages, you've lost your audience. Be concise. Use bullet points. Use tables. Cut the filler.
Rule of thumb: If the reader can't understand your proposal in 10 minutes, the doc is too long.
2. Only presenting one option
If you only present one option, it looks like you've already made up your mind and the doc is a formality. Always present at least two alternatives, even if one is clearly better. The act of comparing is what builds trust.
3. Hiding the trade-offs
Every design has downsides. If your doc doesn't mention any, reviewers will assume you haven't thought about them. Name the trade-offs explicitly. It shows maturity.
4. Writing it after the code
A design doc written after implementation is a waste of everyone's time. The point is to think before you code, not to justify what you already built.
5. Not including open questions
Pretending you have all the answers is a red flag. The best design docs include a section of "things I don't know yet" — it invites collaboration and shows intellectual honesty.
6. Skipping the "why now"
Context matters. Why is this important now? What changed? What's the cost of doing nothing? Without urgency context, your doc competes with every other initiative for attention.
Design Docs as a Career Tool
Here's something most developers miss: design docs aren't just a technical artifact. They're a leadership tool.
Building influence
When you write a design doc, you're not asking permission — you're proposing a direction. You're saying: "I've thought deeply about this problem, considered the alternatives, and here's my recommendation." That's leadership behavior, regardless of your title.
The developers who write design docs:
- Get invited to architecture discussions
- Get trusted with ambiguous, high-impact projects
- Build a reputation for thoughtful engineering
- Have a paper trail of good judgment that speaks for them during promotions
From documentation to discussion
A design doc is a conversation starter, not a conversation ender. The best outcome isn't that everyone rubber-stamps your proposal — it's that the doc sparks a discussion that leads to an even better solution.
Share the doc before writing code. Ask for feedback explicitly: "I'd especially appreciate input on the caching invalidation strategy — I'm least confident about that part." This shows strength, not weakness.
The compound effect
Over time, your design docs become a library of decisions. New team members can read them to understand why the system looks the way it does. During incidents, you can reference them to understand original constraints. During planning, you can revisit them to see which assumptions have changed.
You're not just building software — you're building institutional knowledge.
Quick-Start Checklist
Ready to write your first design doc? Here's a checklist:
- Problem is clear: Can someone outside your team understand what you're solving and why?
- Goals and non-goals defined: Is the scope explicit?
- At least 2 alternatives presented: Did you show multiple approaches?
- Trade-offs table included: Are alternatives compared on multiple criteria?
- Recommendation has reasoning: Did you explain why, not just what?
- Connected to team context: Does the recommendation consider team skills, existing infrastructure, and timeline?
- Implementation plan sketched: Is there a rough delivery sequence?
- Open questions listed: Are unknowns called out honestly?
- Length is reasonable: Can someone read this in 10 minutes?
- Shared before coding: Did you get feedback before writing a single line?
Conclusion
The gap between a mid-level developer and a senior developer isn't about writing better code. It's about thinking more clearly before writing any code at all.
Design docs are how you make that thinking visible. They force you to articulate the problem, consider alternatives, weigh trade-offs, and make a reasoned recommendation. They surface disagreements before they become wasted PRs. They build your reputation as someone who thinks before they act.
You don't need a formal process or management buy-in. Just start. Next time you pick up a task that has multiple valid approaches, spend an hour writing down your thinking. Share it with your team. Ask for feedback.
The first one will feel awkward. The second will feel faster. By the fifth, it'll be second nature — and you'll wonder how you ever started coding without thinking first.
Related Posts
- Choosing the Right Architecture: Decision Framework & Trade-offs — Architecture-level decision making with ADR templates (complements this post's feature-level focus)
- Domain-Driven Design: Strategic & Tactical Patterns — How to model complex domains with bounded contexts and aggregates
- Stop Over-Engineering: You're Not Building Netflix — The flip side: when simple is better than clever
- How to Never Be Replaced: A Developer's Growth Mindset Guide — The habits that compound into senior-level thinking
- SOLID Principles Explained — The design principles that inform good trade-off analysis
Series: Senior Developer Playbook
Next: Domain Modeling in Practice: Boundaries, Bounded Contexts & Real Examples
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.