Skip to content
ExponaBLOG
ENGINEERING CRAFT · DAY 051 / 100

Write a design doc before big builds in Engineering Craft

An hour of writing saves weeks of rework.

Expona Admin7 min read

Write a design doc before big builds

I've watched teams spend three months building the wrong thing, then another month arguing about how to fix it. The pattern is always the same: someone gets excited about a feature, starts coding immediately, and by the time the architecture falls apart, half the codebase is tangled. Write a design doc before big builds—it's not bureaucracy, it's insurance. An hour of writing saves weeks of rework.

Over my 5+ years building production platforms—e-commerce systems handling thousands of concurrent orders, CRM platforms with complex data pipelines, trading systems where latency matters—I've learned that the most expensive mistakes aren't in code, they're in decisions made without thinking them through first. A design document forces that thinking before you've already committed a team to the wrong direction.

Problem, goals and non-goals

Start your design doc by naming the problem clearly. Not "we need better notifications" but "merchants can't see order fulfillment updates in real time, causing 200+ support tickets per week and eroding trust in our platform." Be specific. Quantify where you can.

Then define what you're building and what you explicitly won't build. This is where I see teams waste time later—they start solving edge cases that weren't part of the original ask.

For example, on a ride-hailing platform I worked on, the problem was rider cancellations causing driver income loss. Our goals were:

  • Reduce late cancellations by 30% within 60 days
  • Provide drivers with predictable earnings visibility 24 hours ahead
  • Implement this without requiring driver app changes (backward compatibility)

Non-goals we explicitly ruled out:

  • Implementing cancellation penalties (legal complexity, not our scope)
  • Building a separate earning forecast API (would require separate versioning)
  • Supporting historical earning predictions (MVP only looks 24h forward)

That clarity saved us from three separate feature creep conversations. When someone mentioned penalties, we could point to the doc and say "not in scope for this phase."

Proposed design with diagrams

This is where you actually describe how the system works. Don't write 50 pages—write the minimum that makes the architecture clear to someone who hasn't thought about this yet. I typically cover:

  • Data model changes (what tables, what fields, why)
  • API contracts (request/response shapes)
  • Async flows if relevant (queues, webhooks, event streams)
  • Integration points with existing systems

For that ride-hailing system, our design included a batch job that ran every night at 2 AM UTC (off-peak hours). Here's the essence:

typescript
interface EarningForecast {
  driverId: string;
  forecastDate: string; // YYYY-MM-DD for the next 24 hours
  estimatedTrips: number;
  estimatedEarnings: number; // in cents
  confidence: 'high' | 'medium' | 'low';
  calculatedAt: timestamp;
}

// Batch job runs at 2 AM UTC
// 1. Query last 30 days of completed trips per driver
// 2. Group by hour of day, day of week
// 3. Apply ML model (simple linear regression, not GPT-4)
// 4. Store forecast in PostgreSQL
// 5. Push to Redis cache for API layer
// 6. Publish EarningForecastUpdated event to Kafka

Draw the system boundaries. Show where PostgreSQL fits, where Redis sits for caching, what talks to what. A simple box-and-arrow diagram beats a thousand words. If you're in a tool like Lucidchart or even ASCII art in a GitHub issue, make it visual. People think in pictures.

For our forecast system, the diagram showed:

[Historical Trip Data] → [Batch Job (2 AM)] → [PostgreSQL] → [Redis Cache] → [Driver App via API]

Kafka publishes: EarningForecastUpdated event for analytics and notifications

Include database schema changes explicitly. Don't say "we'll add forecast columns"—show the exact schema.

Column Type Index Nullable Note
id UUID PRIMARY KEY Yes No Partition key
driver_id UUID FOREIGN KEY Yes No For queries by driver
forecast_date DATE Yes No YYYY-MM-DD, always future date
estimated_trips INT No No Based on historical average
estimated_earnings INT No No In cents, always positive
confidence ENUM No No high/medium/low
calculated_at TIMESTAMP No No When forecast was generated

This specificity matters. It gives your team a north star and catches questions early: "Do we need an index on forecast_date?" Yes, because the API does range queries.

Alternatives and trade-offs

Never pretend there's only one way to solve a problem. Show at least two alternatives and explain why you chose yours. This section is where senior team members gain confidence in your thinking.

For the forecast system, we considered three approaches:

Option 1: Real-time API forecast (rejected)

  • Pros: Always fresh data, no batch lag
  • Cons: Expensive ML inference on every request, p99 latency could hit 500ms+, drivers hitting the endpoint too often would cause spike load
  • Cost: ~$2k/month in compute, plus engineering effort to handle caching complexity

Option 2: Batch job at 2 AM (chosen)

  • Pros: Predictable compute cost (~$200/month), simple to understand, cheap cache hits all day, forecasts are stable (drivers aren't refreshing 100 times)
  • Cons: 24h stale data, drivers in other time zones see forecast generated "yesterday"
  • Cost: Low, proven pattern

Option 3: Hybrid—batch + lightweight cache warmer (considered)

  • Pros: Fresh within 1 hour, good for real-time needs
  • Cons: More complex code, harder to debug cache invalidation bugs, 3x the ops surface
  • Cost: ~$600/month, engineering debt

We chose Option 2 because our user research showed drivers check earnings in the morning, not mid-trip. A 24h forecast was sufficient. Option 3 looked attractive but wasn't worth the complexity for our use case.

Always include the business trade-off, not just technical one. "This will cost $X/month in infrastructure" is information your manager needs to make the right call.

Risks, rollout plan and open questions

What can go wrong? Name the failure modes explicitly.

Risks for the forecast system:

  • ML model confidence drops in low-volume markets—drivers with <5 trips/week get meaningless forecasts. Mitigation: Show low confidence badge in UI, surface only to drivers with 7-day trip count > 3.
  • Database grows 5GB+ per month—retention policy needed. Mitigation: Archive forecasts older than 90 days to cold storage, keep hot data for 14 days only.
  • Kafka outage silences forecast events, breaking analytics—historical data loss. Mitigation: Publish events synchronously before cache expires, add SLA monitoring.
  • Batch job fails silently, drivers see stale forecasts—trust erosion. Mitigation: Alert when job hasn't run in 25 hours, include calculated_at in API response so client can warn if data is old.

Describe how you'll roll out. Staging? Canary? Dark mode (run code but don't expose)? For our forecast, we did:

  1. Week 1: Deploy to staging, run batch 3 times manually, verify data accuracy with 10% sample
  2. Week 2: Canary to 5% of drivers in test market (Brazil), check for API errors and cache misses
  3. Week 3: Roll to 25% (Latin America), monitor p99 latency and alert on stale data
  4. Week 4: Full rollout globally, with runbook for rollback in 30 minutes

Write it down. Don't assume your team will remember the steps.

List open questions. These are decisions you've deferred or unknowns:

  • Should we show confidence level to drivers? UX to decide, due by Day 5 of implementation.
  • Do we need separate forecasts per vehicle type (bike vs car vs truck)? Product to validate demand, can be Phase 2 if valuable.
  • What's our SLA for forecast accuracy? Finance and ops to define acceptable error margin.

These questions prevent surprises during development. You find the answers now, not week 3 when someone says "wait, shouldn't this work for bikes too?"

Common mistakes

Making it too long. Design docs shouldn't be 20 pages. Aim for 3-5. If you're writing a novel, you're not being clear enough. Ruthlessly cut fluff.

Avoiding difficult decisions. Don't write "we'll support both PostgreSQL and MongoDB." Pick one and explain why. Ambiguity kills teams.

Skipping the alternatives section. If your design has no trade-offs, you haven't thought hard enough. Every choice costs something.

No cost estimate. "We'll use Kafka" is incomplete. "We'll use Kafka, adding ~$300/month in streaming costs and 2 weeks of integration work" is complete. Your tech lead can make an actual decision.

Ignoring deployment. Beautiful architecture that's impossible to deploy safely isn't useful. Your rollout plan is part of the design.

Key takeaways

  • Start with a clear problem statement and explicit non-goals—this prevents scope creep before code is written.
  • Draw your system: data models, APIs, async flows, integration points. Specificity catches questions early.
  • Show alternatives and trade-offs, including cost. Senior developers respect thinking, not just answers.
  • Name your risks and how you'll mitigate them. Include rollout steps and open questions.
  • Keep it short (3-5 pages)—clarity beats comprehensiveness.
  • Most importantly: write this before you code. An hour of writing saves weeks of rework.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles