Software Design Document
beta · Family decision-docs · Phase develop · Sizes lean, full · ~2,350 tokens
Describes how a system will be built: the technical design, its structure, the alternatives weighed, and the cross-cutting concerns, so the design can be reviewed before the code exists. Describes an implementation, as distinct from an RFC (which proposes a decision) and an ADR (which records one).
Fast reference for using the SDD bundle. For the full reasoning, history, and sources, read
sdd_companion.md.
When to use
Section titled “When to use”- You are about to build something more than a one-file change, and thinking the design through and getting it reviewed first would catch an expensive mistake on paper.
- The work is hard to estimate or hard to deliver without settling the design (a good default trigger).
- The design touches more than one component or team, and people who were not in the room need to reason about it.
- A choice in the design is hard to reverse (a data model, a public API, a persistence or auth decision) and worth a record.
When NOT to use
Section titled “When NOT to use”- You want feedback on whether to do this at all. That is an RFC: it proposes a decision and asks for input. (Many teams call this a design doc too; the distinction that matters is whether the primary output is a decision-before-building or a description of the implementation.) A design doc describes how you will build something.
- You are recording a single, already-made, hard-to-reverse decision. That is an ADR. (A design doc may still spin one off; see below.)
- The change is trivial or fully reversible. A pull-request description is enough. A design doc for a one-line change is process for its own sake.
- The design is genuinely unknowable until you build it, and a spike would teach you more than a document. Prototype first, then write the doc if the design still warrants review.
Design doc, or RFC, or ADR? (the question people actually have)
Section titled “Design doc, or RFC, or ADR? (the question people actually have)”| Design doc / SDD | RFC | ADR | |
|---|---|---|---|
| Answers | “How will we build it?” | “Should we, and which way?” | “What did we decide, and why?” |
| Timing | Before/while building | Before the decision | After the decision |
| Describes | An implementation | A proposal | A decision |
| State | Living, then archived | Mutable while open | Immutable once accepted |
| Scope | The whole design | One proposed change | One decision |
They are a division of labor, not a competition. Much of the industry uses “design doc” and “RFC” interchangeably, which is fine when one document does both jobs; the moment feedback on the decision is the point, it is an RFC. The ADR is genuinely distinct: when your design makes a significant, hard-to-reverse choice, record that decision also as an ADR, so the next person finds it next to the code rather than buried in a design doc.
Pick a variant
Section titled “Pick a variant”- Lean (default): Context and Scope, Goals and Non-Goals, The Design, Alternatives Considered, Cross-Cutting Concerns. This is the Google-style “mini design doc,” enough for most changes worth a design doc at all (roughly one to three pages).
- Full: breaks The Design into Static Structure, Runtime and Data Flow, Interfaces and Contracts, and Deployment and Operations, and adds Quality Attributes and Risks and Open Issues. Use it when the design is hard to reverse, crosses teams, carries security/privacy/regulatory weight, or has real non-functional targets.
Grow lean into full by adding sections; never reorder the shared ones. The scaling signal is the cost of being wrong, not the size of the system.
Quality rubric (self-grade before you circulate)
Section titled “Quality rubric (self-grade before you circulate)”- The scope has a boundary: the doc says what is in and, explicitly, what is out.
- Goals are outcomes, not a build list, and there is at least one non-goal.
- The design is concrete enough to disagree with, and a diagram carries the structure where prose would be worse.
- Alternatives are real, including “do nothing” or the obvious approach, each described well enough that a reader could prefer it. No straw men.
- Cross-cutting concerns are walked explicitly (auth, privacy, observability, failure, cost), not waved off as “standard.”
- (Full) Quality attributes have numbers or scenarios, not adjectives.
- (Full) Risks and open issues are named, and any hard-to-reverse decision is flagged for an ADR.
- You know which of design-doc / RFC / ADR you are writing, and this is the right one.
- Status is current, and there is a plan for the doc after shipping (archive, or mark last verified).
- Every guidance comment is deleted; no placeholders remain.
Named anti-patterns (the usual wrecks)
Section titled “Named anti-patterns (the usual wrecks)”- Conflating design doc, RFC, and ADR. Writing a “design doc” that is really an undecided proposal (an RFC), or burying a significant hard-to-reverse decision inside a design doc instead of recording it as an ADR where it can be found. Know which of the three you are writing.
- Big design up front. A complete, detailed design produced before any code and treated as fixed. It commits you before you have learned the most, and produces a design the implementation quietly abandons. Write “just enough” design; reach for the full variant only when the cost of being wrong earns it.
- The stale living doc. A design doc kept as the system’s description long after the code diverged. A half-correct document everyone trusts is worse than an obviously archived one. Archive it after shipping, or mark when it was last verified against reality.
- Writing to exhaustion. Using length and exhaustive detail as a substitute for clear thinking, so the design is approved because no reviewer could finish it. Detail the risky parts; compress the obvious ones.
- No alternatives. A design presented as the only option cannot show it was reasoned; reviewers supply the missing options adversarially.
- Wishes instead of targets. Quality attributes stated as adjectives (“fast,” “scalable”) with no number or scenario, so nothing in the design can be checked against them.
No paired skill (yet)
Section titled “No paired skill (yet)”There is no develop-sdd skill in the product-on-purpose org today, so this bundle’s pairs_with is
empty. The org has a skill for the decision record (develop-adr) but none for the design document.
Until one exists, this template is filled by hand. Recorded as context in STATE.md.
The artifacts
Section titled “The artifacts”sdd_template-lean.md · ~2,350 tokens
---title: "{{title}}"status: "{{status}}"authors: ["{{authors}}"]created: "{{date}}"updated: "{{date}}"doc_type: sddsize: leansource_template: sddsource_template_version: 0.1.0---
<!--LEAN SOFTWARE DESIGN DOCUMENT. The smallest design doc that still gets a design thought through andreviewed before it is expensive to change. Use it for a change that is more than a one-file edit but isnot a hard-to-reverse, multi-team, or safety-critical design. To grow it into a full design doc (seesdd_template-full.md), ADD sections and subsections; never rename or reorder the ones below, because thefull variant is a strict superset of this one.
WHAT A DESIGN DOC IS, AND IS NOTA design doc describes HOW you will build something, so people can review the design before the codeexists. It is not an RFC (which PROPOSES a decision and asks for feedback) and not an ADR (which RECORDSa decision after it is made). If what you actually want is feedback on whether to do this at all, writean RFC. If you are recording a single hard-to-reverse choice, write an ADR. See sdd_companion.mdsection 8. The value of this document is the review it gets, not the document itself.
STATUS is a lifecycle, not a label. Move it as the design moves: draft -> in-review -> approved -> implemented (and later, possibly, superseded)Keep it current, and once the code ships, either archive this doc or mark when it was last checkedagainst the real system. A design doc everyone trusts and no one has verified is worse than none.
HOW TO FILL THIS IN1. Read the comment under each heading: WHAT it wants, WHY it matters (with a pointer into sdd_companion.md for the deep reasoning), guiding questions to ASK, a GOOD and a WEAK example, and the TRAP to avoid.2. Replace each {{placeholder}} with your content.3. If a section does not apply, write "N/A" and one line of why, rather than deleting it.4. Before you circulate it: self-grade against sdd_guide.md, then DELETE every HTML comment. They are guidance, not content.-->
# {{title}}
## Context and Scope
<!-- WHAT What exists today, what is changing, and the boundary of this design. Orient a reader who was not in the meetings, then say plainly what is in scope and what is out. WHY A design doc with a vague boundary invites reviewers to drag in every adjacent system, and the review never converges. The scope line is the cheapest way to keep the discussion on the thing you are actually designing. Deep dive: sdd_companion.md section 3 (Anatomy > Context and Scope). ASK What is the current state? What is changing and why now? Where does this design start and stop? What nearby system is explicitly NOT part of this? GOOD "Today, task-list filters and sorts live in URL query params and are lost on navigation; there is no way to save or share a view. This design adds saved, shareable views. In scope: storing, listing, and sharing a view within a project. Out of scope: cross-project views, and any change to the filtering engine itself." WEAK "We are building saved views." (no current state, no boundary, so the review has nothing to anchor on) TRAP A Context section that re-derives the whole product's history. Orient in a paragraph; do not re-litigate the requirements the PRD already settled. -->
{{context_and_scope}}
## Goals and Non-Goals
<!-- WHAT Goals: what the design must achieve to succeed, ideally observable. Non-goals: what it is deliberately NOT trying to do, to keep the design and its review bounded. WHY Non-goals are the highest-value line of scope control in a design doc. Most runaway reviews are reviewers arguing about something you never meant to solve; naming it a non-goal ends that thread before it starts. Deep dive: sdd_companion.md section 3 (Anatomy > Goals and Non-Goals). ASK What must be true for this design to have worked? What are we explicitly not solving here, and why? What is a reasonable-but-separate concern a reviewer might raise? GOOD Goal: "A user can save the current filter/sort as a named view and reuse it across sessions and devices." Non-goal: "Real-time collaborative editing of a view. Views are saved and shared, not co-edited live; that is a separate feature." WEAK A goals list with no non-goals, so every reviewer expands the scope in a different direction. TRAP Goals written as a feature list ("build the views table, build the API") rather than as outcomes. State what must be TRUE, not what you will type. -->
{{goals_and_non_goals}}
## The Design
<!-- WHAT The heart of the document: how the system is actually structured to meet the goals. The core idea, the main components, how they interact, and the data that flows between them. A diagram is usually worth more than the paragraph it replaces. For a heavier design, the full variant breaks this into structural, runtime, interface, and deployment views. WHY This is the object of review. It has to be concrete enough for a colleague to disagree with, and no more exhaustive than that: the job is to convey the design and expose the risky parts, not to pre-write the code. Deep dive: sdd_companion.md section 3 (Anatomy > The Design). ASK What are the components, and how do they depend on each other? What is the data model? What are the key interfaces? What is the important runtime behavior (the main sequences, the failure paths)? GOOD "Add a `saved_view` table (id, name, owner_id, project_id, scope, config JSON, timestamps) in the existing Postgres database. The task-service exposes CRUD endpoints under /projects/{id}/views. The frontend adds a Views dropdown that captures the current filter/sort into `config` on save. Shared views are visible to all project members; private views only to the owner. [figure: component and data-flow sketch, illustrative]" WEAK "Use a database to store views and an API to serve them." (names categories, not a design anyone can review or build from) TRAP Turning this into an exhaustive spec that no reviewer can finish. Detail the risky and novel parts; compress the obvious ones. Approval by fatigue is not approval. -->
{{the_design}}
## Alternatives Considered
<!-- WHAT The other designs you genuinely weighed and why you are not proposing them, including "do nothing" or "the obvious approach" where those are real options. WHY Alternatives are the evidence the design is reasoned rather than merely preferred, and they are how a reviewer surfaces the option you missed. Their absence invites the reader to supply them adversarially. Deep dive: sdd_companion.md section 3 (Anatomy > Alternatives Considered); the no-alternatives anti-pattern is in section 7. ASK What else could meet the goals? What is the obvious approach you rejected, and why? What would a skeptic build instead? Is "do nothing / keep it in the URL" acceptable, and why not? GOOD "Store config in the URL and localStorage only (rejected: no sharing, lost across devices). A generic user-preferences key-value blob (rejected: no project-scoping or sharing semantics, and awkward to query). A separate views microservice (rejected: a new service and a data-sync problem for what is fundamentally one table)." WEAK One decoy option dismissed in half a sentence. TRAP Straw men. Listing only options you would never pick makes the design look inevitable, which reads as a sales pitch and costs the reviewer's trust. -->
{{alternatives_considered}}
## Cross-Cutting Concerns
<!-- WHAT The concerns that touch every part of the design rather than living in one component: security and privacy, observability, error handling, data retention, cost, accessibility, internationalization. For each, one honest line: how the design handles it, or that it does not apply and why. WHY This is where a design that looked clean reveals its real cost: the auth check, the privacy surface, the observability that has to be designed in rather than bolted on. Deep dive: sdd_companion.md section 3 (Anatomy > Cross-Cutting Concerns). ASK What is the auth and privacy model? What do we log and measure, and are there quotas or cost limits? How does it fail, and what happens then? Any accessibility or i18n impact? GOOD "Auth: shared views are readable by any project member; write is owner-only; a membership check gates every read of a shared view. Privacy: private views are never returned to non-owners. Observability: emit view.created and view.applied events. Cost: cap 100 saved views per user to bound storage." WEAK "Standard security applies." (says nothing a reviewer can check) TRAP Leaving the section blank because "nothing cross-cutting changed." Walk the list explicitly; a one-line "N/A, auth is unchanged and inherited" is a real answer, silence is not. -->
{{cross_cutting_concerns}}sdd_template-full.md · ~3,950 tokens
---title: "{{title}}"status: "{{status}}"authors: ["{{authors}}"]reviewers: ["{{reviewers}}"]created: "{{date}}"updated: "{{date}}"related: ["{{related_docs}}"]doc_type: sddsize: fullsource_template: sddsource_template_version: 0.1.0---
<!--FULL SOFTWARE DESIGN DOCUMENT. Every section, for a design that earns the weight: hard to reverse,crosses teams, carries security, privacy, or regulatory consequence, or has real non-functional targetsthe design must be shaped around. Most changes do not earn it. Reaching for this variant by reflex is howa design-doc practice turns into the big-design-up-front bureaucracy its critics warn about, where writingto exhaustion substitutes for deciding.
The full variant is a strict superset of the lean one: the shared sections keep their names and theirorder, and this file only ADDS (the structural, runtime, interface, and deployment subsections under TheDesign, plus Quality Attributes and Risks and Open Issues).
WHAT A DESIGN DOC IS, AND IS NOTA design doc describes HOW you will build something. It is not an RFC (which PROPOSES a decision and asksfor feedback) and not an ADR (which RECORDS a decision after it is made). If a significant, hard-to-reversechoice is made inside this design (a database, an auth model, a public contract), record that decisionALSO as an ADR, so someone can find it next to the code later. See sdd_companion.md section 8.
STATUS is a lifecycle, not a label. Keep it current: draft -> in-review -> approved -> implemented (and later, possibly, superseded)Name real reviewers. Once the code ships, either archive this doc or mark when it was last verifiedagainst the running system; a design doc trusted but never checked is worse than none.
HOW TO FILL THIS IN1. Read the comment under each heading: WHAT it wants, WHY it matters (with a pointer into sdd_companion.md for the deep reasoning), guiding questions to ASK, a GOOD and a WEAK example, and the TRAP to avoid.2. Replace each {{placeholder}} with your content.3. If a section does not apply, write "N/A" and one line of why, rather than deleting it.4. Before you circulate it: self-grade against sdd_guide.md, then DELETE every HTML comment.-->
# {{title}}
## Context and Scope
<!-- WHAT What exists today, what is changing, and the boundary of this design. Orient a reader who was not in the meetings, then say plainly what is in scope and what is out. WHY A design doc with a vague boundary invites reviewers to drag in every adjacent system, and the review never converges. Deep dive: sdd_companion.md section 3 (Anatomy > Context and Scope). ASK What is the current state? What is changing and why now? Where does this design start and stop? What nearby system is explicitly NOT part of this? GOOD "Today, task-list filters and sorts live in URL query params and are lost on navigation. This design adds saved, shareable views. In scope: storing, listing, and sharing a view within a project. Out of scope: cross-project views, and any change to the filtering engine itself." WEAK "We are building saved views." (no current state, no boundary) TRAP A Context section that re-derives the whole product's history. Orient in a paragraph; do not re-litigate the requirements the PRD already settled. -->
{{context_and_scope}}
## Goals and Non-Goals
<!-- WHAT Goals: what the design must achieve to succeed, ideally observable. Non-goals: what it is deliberately NOT trying to do, to keep the design and its review bounded. WHY Non-goals are the highest-value line of scope control in a design doc. Most runaway reviews are reviewers arguing about something you never meant to solve. Deep dive: sdd_companion.md section 3 (Anatomy > Goals and Non-Goals). ASK What must be true for this design to have worked? What are we explicitly not solving, and why? GOOD Goal: "A user can save the current filter/sort as a named view and reuse it across sessions and devices." Non-goal: "Real-time collaborative editing of a view; that is a separate feature." WEAK A goals list with no non-goals, so every reviewer expands the scope differently. TRAP Goals written as a build list ("build the table, build the API") rather than as outcomes. State what must be TRUE, not what you will type. -->
{{goals_and_non_goals}}
## The Design
<!-- WHAT The heart of the document: how the system is structured to meet the goals. In this full variant, present it through the four views below (static structure, runtime, interfaces, deployment). Open with a paragraph naming the core idea before the views. WHY This is the object of review. It must be concrete enough to disagree with, and no more exhaustive than that. Deep dive: sdd_companion.md section 3 (Anatomy > The Design). ASK What is the core design idea in one paragraph, before the detail? What one-sentence framing orients a first-time reader before the views below? GOOD "Saved views are one new entity in the existing task-service and database, exposed by new REST endpoints and a Views dropdown in the task-list UI. The four views below detail the structure, the runtime, the contracts, and how it deploys." WEAK Jumping straight into the data model with no framing sentence, so a reviewer cannot tell what they are about to read. TRAP Detail before shape. Give the one-paragraph idea first, then the views. -->
{{the_design_overview}}
### Static Structure and Components
<!-- WHAT What the parts are and how they depend on each other: components, modules, the data model, and the dependencies between them. The "building block" view; a component diagram belongs here. WHY The static view is where a reviewer sees whether the decomposition is sound and whether a dependency is pointing the wrong way. Deep dive: sdd_companion.md section 3 (Anatomy > The Design). ASK What are the components and their responsibilities? What is the data model (entities, fields, relationships)? What depends on what? What is new versus reused? GOOD "New: a `saved_view` table (id, name, owner_id, project_id, scope enum private|shared, config JSONB, created_at, updated_at) and a ViewsController in the task-service. Reused: the existing auth middleware and the project-membership service. [figure: C4 component diagram, illustrative]" WEAK A class-by-class dump of every field and method, so the shape is lost in the detail. TRAP One diagram that mixes system context, containers, and code into an unreadable everything-map. Pick a level and keep to it. -->
{{static_structure_and_components}}
### Runtime and Data Flow
<!-- WHAT How the parts behave together over time: the important sequences, the state transitions, and the failure paths. What happens on the key operations, step by step. WHY The static view shows what the parts are; the runtime view shows whether they actually work together, and it is where a race, a missing failure path, or a chatty call pattern shows up. Deep dive: sdd_companion.md section 3 (Anatomy > The Design). ASK What is the sequence for the main operations (create, apply, share a view)? What are the failure modes and how are they handled? Where is state, and how does it change? GOOD "Save: the client posts the current filter/sort to POST /projects/{id}/views; the service validates membership, persists the row, returns the view id. Apply: the client fetches GET /projects/{id}/views/{viewId} and rehydrates the filter state. Failure: a shared view that references a since-deleted field applies the rest and flags the missing filter rather than erroring. [figure: sequence diagram, illustrative]" WEAK Restating the static structure again, with no sequence or failure behavior. TRAP Only drawing the happy path. The failure paths are the reason this section exists. -->
{{runtime_and_data_flow}}
### Interfaces and Contracts
<!-- WHAT The APIs, schemas, events, and contracts this design exposes and consumes: endpoints and their shapes, the config schema, events emitted, and any external contract touched. WHY Interfaces are the design's public surface and the part other teams build against, so they are the part a careful reviewer probes hardest and the most expensive to change after the fact. Deep dive: sdd_companion.md section 3 (Anatomy > The Design). ASK What are the endpoints and their request/response shapes? What is the schema of the stored config? What events are emitted, and who consumes them? What existing contract changes? GOOD "REST: GET/POST /projects/{id}/views, GET/PUT/DELETE /projects/{id}/views/{viewId}. Config schema (versioned): { version: 1, filters: [...], sort: {...}, columns: [...] }. Events: view.created, view.applied. No change to existing task endpoints." WEAK "A CRUD API for views." (names no shapes, so no one can build a client or a test against it) TRAP Omitting the config schema. The stored JSON is the real contract here, and an unversioned one will hurt the first time filters change (see Risks). -->
{{interfaces_and_contracts}}
### Deployment and Operations
<!-- WHAT Where it runs and how it is released: the deployment target, the migration, feature-flagging, rollout and backout, and the operational ownership. WHY Many designs are sound and still never ship, because no one owned the messy path to production. Deep dive: sdd_companion.md section 3 (Anatomy > The Design); the deployment view is arc42's. ASK What is the migration? Is it behind a flag, and how does it roll out and back out? Who operates it? What has to be true in each environment? GOOD "Additive migration: create the `saved_view` table (no change to existing tables). Ship behind a `saved_views` flag, enabled per-project, dark-launched to internal projects first. Backout: disable the flag; the table is inert. Owned by the task-service team; no new service to run." WEAK "Deploy it normally." (names no migration, flag, or backout) TRAP Treating the migration as an afterthought. State whether it is additive and reversible; a destructive migration is a different risk conversation. -->
{{deployment_and_operations}}
## Alternatives Considered
<!-- WHAT The other designs you genuinely weighed and why you are not proposing them, including "do nothing" or "the obvious approach" where those are real options. WHY Alternatives are the evidence the design is reasoned rather than preferred, and how a reviewer surfaces the option you missed. Deep dive: sdd_companion.md section 3 (Anatomy > Alternatives Considered); the anti-pattern is in section 7. ASK What else could meet the goals? What obvious approach did you reject, and why? What would a skeptic build? Why not "do nothing"? GOOD "URL and localStorage only (rejected: no sharing, lost across devices). A generic user-preferences blob (rejected: no project-scoping or sharing semantics, awkward to query). A separate views microservice (rejected: a new service and a sync problem for one table)." WEAK One decoy dismissed in half a sentence. TRAP Straw men. Two decoys and a winner is a sales pitch, not a design review. -->
{{alternatives_considered}}
## Cross-Cutting Concerns
<!-- WHAT The concerns that touch every part of the design: security and privacy, observability, error handling, data retention, cost, accessibility, internationalization. For each, one honest line: how the design handles it, or that it does not apply and why. WHY This is where a design that looked clean reveals its real cost. Deep dive: sdd_companion.md section 3 (Anatomy > Cross-Cutting Concerns). ASK What is the auth and privacy model? What do we log and measure? How does it fail? Any quotas, cost, accessibility, or i18n impact? GOOD "Auth: shared views readable by any project member, write owner-only, membership checked on every read. Privacy: private views never returned to non-owners. Observability: view.created and view.applied events. Cost: cap 100 saved views per user." WEAK "Standard security applies." (nothing a reviewer can check) TRAP Leaving it blank because "nothing cross-cutting changed." Walk the list; a one-line "N/A, inherited" is a real answer, silence is not. -->
{{cross_cutting_concerns}}
## Quality Attributes
<!-- WHAT The non-functional requirements the design must satisfy, as targets a reader could check: performance and latency, scalability, availability, reliability, security posture, maintainability. Give numbers and scenarios, not adjectives. WHY A quality attribute with no scenario and no target is one the design has not actually addressed. This is where the design meets the non-functional parts of the PRD or SRS. Deep dive: sdd_companion.md section 3 (Anatomy > Quality Attributes). ASK What are the latency and throughput targets? How much data and load must it handle? What is the availability target? How maintainable and evolvable must it be? GOOD "Performance: the views list loads p95 < 150ms at 50 views. Scale: up to ~500 shared views per project. Availability: inherits the task-service SLO (99.9%). Maintainability: the config schema is versioned so filters can evolve without breaking stored views." WEAK "It should be fast and scalable." (adjectives with no target, so nothing can be verified) TRAP Listing every quality attribute in the textbook. Name the two or three that actually constrain THIS design and give them real numbers; mark the rest N/A. -->
{{quality_attributes}}
## Risks and Open Issues
<!-- WHAT The parts of the design that are uncertain, risky, or undecided, and what you plan to do about each. Name the things that could go wrong and the questions not yet answered. WHY A design doc with no risks section, on any non-trivial design, is not riskless; it is undisclosed. Naming a risk is not weakness. Where a risk is really an open decision, that is the signal to record an ADR or open an RFC. Deep dive: sdd_companion.md section 3 (Anatomy > Risks and Open Issues). ASK What is most likely to go wrong? What depends on something you do not control? What is still undecided, and who decides it? What technical debt does this knowingly take on? GOOD "Risk: the stored config references filter fields that may later be renamed or removed; mitigate by versioning the config schema and applying-what-resolves. Open: should shared views be editable by any member or only the owner? Owner: the product lead, by design review. Debt: no pagination on the views list until a project exceeds ~200 views." WEAK "Some risks may exist." (discloses nothing) TRAP Listing only generic risks ("scope creep," "timeline") instead of the design's real technical risks. This section is about THIS design's uncertainties. -->
{{risks_and_open_issues}}---title: "Saved Views for Dashboards: design"status: in-reviewauthors: ["Marcus Bell (Staff Engineer, Reporting)"]reviewers: ["Priya Nair (PM, Reporting)", "Dana Osei (Staff Engineer, Platform)"]created: "2026-07-02"updated: "2026-07-09"related: ["../prd/prd_example.md (Saved Views for Dashboards PRD)", "future ADR: saved-view storage model"]doc_type: sddsize: fullsource_template: sddsource_template_version: 0.1.0---
<!--This is a worked example for the SDD bundle. It is a realistic, fully filled full-variant design doc fora single feature, the design counterpart to the PRD in prd_example.md (both use the fictional "SavedViews" feature, so the delivery-docs and decision-docs families chain on one scenario). Figures marked"illustrative" are described in text for the example and would be real diagrams in a live design doc. Useit as a model of shape and tone, not as a source of facts.-->
# Saved Views for Dashboards: design
## Context and Scope
Today, a dashboard's filters, date range, and visible columns live only in the page's ephemeral state andreset to the dashboard default every time it is opened. The [Saved Views PRD](../prd/prd_example.md) establishedthe need: Recurring Analysts rebuild the same filter set several times a day, and Team Leads want theirteam looking at one agreed view. This design describes how we build saved, reusable, and shareable viewson top of the dashboard-service and the per-user preferences storage that shipped in Q1.
**In scope:** persisting a view (filters, date range, visible columns) for a dashboard; listing andswitching a user's views; setting a personal default; sharing a view with other permitted users of thesame dashboard.
**Out of scope:** cross-dashboard "global" views, scheduled delivery of a view, and any change to thedashboard definition or the filtering engine itself. These are the PRD's non-goals and are unchanged here.
## Goals and Non-Goals
**Goals**- A user can save the current dashboard state as a named view and reopen it in one action, across sessions and devices.- A user can set one view as their personal default for a dashboard.- A user can share a view so other permitted users of that dashboard can select it, with no path to data the recipient could not already see.
**Non-Goals**- **Real-time co-editing of a view.** Views are saved and shared, not co-edited live; that is a separate feature and a much harder concurrency problem.- **A team-owned default** (a Team Lead setting the team's default view). This is an open product question (see Risks and Open Issues) and is deliberately not designed here yet.- **Changing the Q1 storage decision.** Per-user preferences already chose per-user over per-dashboard storage; this design adds an entity, it does not revisit that.
## The Design
Saved views are one new entity owned by the existing dashboard-service and stored in the main applicationdatabase, exposed by a small set of new REST endpoints and a "Views" control in the dashboard header. Aview captures the dashboard state as a versioned JSON blob; sharing is a scope flag plus a permissioncheck delegated to the existing dashboard permissions service. The four views below detail the staticstructure, the runtime behavior, the interface contracts, and how it deploys. The one decision worthlifting out of this document and into an ADR is the storage model (a dedicated table versus extending theQ1 per-user preferences store); it is called out in Alternatives Considered and Risks and Open Issues.
### Static Structure and Components
One new table and one new controller; everything else is reused.
**New `saved_view` table** (main Postgres database):
| Column | Type | Notes ||---|---|---|| `id` | uuid, PK | || `name` | text | user-supplied, non-empty || `owner_id` | uuid, FK users | the creator || `dashboard_id` | uuid, FK dashboards | the view's dashboard || `scope` | enum(`private`, `shared`) | default `private` || `config` | jsonb | versioned view state (see Interfaces) || `created_at` / `updated_at` | timestamptz | |
Per-user default is a single `default_view_id` added to the existing per-user preferences record, keyedby dashboard, rather than a column on `saved_view`: a default is a per-user choice about a view, not aproperty of the view, and shared views must be defaultable by users who do not own them.
**New `ViewsController`** in the dashboard-service, exposing the endpoints in Interfaces and Contracts.**Reused:** the existing auth middleware, the dashboard permissions service (for shared-view accesschecks), and the frontend dashboard state store (the Views control reads and writes the same filter/date/column state the dashboard already manages).
*Figure 1 (illustrative): a C4 component diagram would show the dashboard-service containing the newViewsController alongside the existing DashboardController, both calling the permissions service, with the`saved_view` table and the per-user preferences store as the two data stores touched.*
### Runtime and Data Flow
- **Save a view:** the client posts the current dashboard state to `POST /dashboards/{id}/views`. The service checks the caller's read access to the dashboard, validates the `config` against the current schema version, persists the row with `scope = private`, and returns the new view.- **Apply a view:** the client fetches `GET /dashboards/{id}/views/{viewId}` and rehydrates the dashboard state from `config`. If a `config` references a filter field that no longer exists, the service returns the resolvable parts and a `stale_fields` list; the client applies what it can and shows the PRD's "some filters no longer exist" message rather than failing the load.- **Share a view:** the owner sets `scope = shared` via `PUT`. On any subsequent read of a shared view by another user, the service re-checks that user's access to the underlying dashboard data through the permissions service, so a shared view can never widen what a recipient may see.- **Set default:** writing `default_view_id` to the caller's preferences record; on dashboard open, the service resolves the default (if any and still readable) and applies it.
*Figure 2 (illustrative): a sequence diagram would show the share-then-apply path, with the permissionre-check happening on the recipient's read, not at share time.*
### Interfaces and Contracts
**REST endpoints** (all under the dashboard-service, authenticated as today):
- `GET /dashboards/{id}/views` - list the caller's own views plus shared views on this dashboard.- `POST /dashboards/{id}/views` - create a view from a `config` payload.- `GET /dashboards/{id}/views/{viewId}` - fetch one view, with `stale_fields` if any.- `PUT /dashboards/{id}/views/{viewId}` - rename, change scope, or update `config` (owner only).- `DELETE /dashboards/{id}/views/{viewId}` - delete (owner only).
**Config schema (versioned).** The stored JSON is the real contract, so it carries a version from dayone:
```{ "version": 1, "filters": [ { "field": "region", "op": "in", "value": ["EMEA"] } ], "date_range": { "preset": "last_30_days" }, "columns": ["name", "owner", "status", "updated_at"] }```
**Events** (for the PRD's analytics): `view_saved`, `view_switched`, `view_set_default`, `view_shared`,`view_load_error` (with reason), each carrying dashboard id and scope. No existing dashboard endpointchanges.
### Deployment and Operations
- **Migration:** additive only. Create the `saved_view` table and add the nullable `default_view_id` to the preferences record; no existing table is altered, so the migration is reversible.- **Rollout:** behind a `saved_views` feature flag, enabled per-dashboard, dark-launched to internal dashboards for a week, matching the PRD's phased plan (private views first, sharing after the security review).- **Backout:** disable the flag; the Views control disappears and dashboards fall back to default state. Saved-view rows are retained, not deleted, so re-enabling is safe.- **Ownership:** the Reporting team owns the ViewsController and the table; no new service is introduced, so there is no new on-call surface.
## Alternatives Considered
- **Extend the Q1 per-user preferences store** (keep views as entries in the existing per-user key-value preferences record). Rejected: that store is per-user by construction and has no project- or dashboard-scoped query path and no notion of a shared, permission-checked object. Sharing is a core goal (PRD FR-4), and bolting it onto a per-user KV blob would mean reinventing scoping and access control in application code. A dedicated table models the object honestly.- **Client-only storage (URL and localStorage).** Rejected: no sharing, lost across devices, and directly contradicts the PRD's cross-device and sharing goals.- **A separate saved-views microservice.** Rejected: a new service, a new data store to keep in sync with dashboards, and a new on-call rotation, all for what is fundamentally one table and one controller. Overkill against the goals.
The choice between the first option and the adopted dedicated-table design is significant and hard toreverse once views exist, so it should be recorded as its own **ADR** (the design-doc-to-ADRrelationship: this document holds the whole design; the storage decision is the one piece worth findingnext to the code on its own). The Q1 per-user-versus-per-dashboard decision is already recorded asADR-014 (illustrative); this is its sequel.
## Cross-Cutting Concerns
- **Security and privacy.** Write is owner-only, enforced in the controller. A shared view's read re-checks the reader's access to the underlying data through the permissions service on every load, so a shared view never exposes data the recipient could not already access (PRD security NFR). Private views are never returned to anyone but the owner.- **Observability.** The five analytics events above, plus standard request metrics on the new endpoints; a `view_load_error` rate panel is wired before GA so stale-view degradation is visible.- **Failure handling.** A `config` referencing a deleted field degrades gracefully (apply-what-resolves, flag the rest) rather than erroring the dashboard load.- **Cost and quota.** A cap of 100 saved views per user per dashboard bounds storage and keeps the list usable; the cap is configurable and logged when hit. (The PRD left the exact cap open, owned by the PM before Phase 1; 100 is this design's input to that decision, not a final answer.)- **Accessibility.** The Views control is keyboard-operable and screen-reader labeled to WCAG 2.2 AA, reusing the design-system menu component; no new accessibility surface is invented.
## Quality Attributes
- **Performance.** Applying a saved view renders in under 1s at p95 on a standard dashboard (the PRD target); the views list responds in p95 < 150ms at up to 50 views at launch (a single indexed query on `(dashboard_id, owner_id, scope)`). The path from that launch state to the full 500-view scale envelope is the pagination debt noted in Risks and Open Issues.- **Scale.** Up to roughly 500 shared views per dashboard and 100 private views per user per dashboard; beyond that the list paginates (see Risks).- **Availability.** Inherits the dashboard-service SLO (99.9%); saved views add no new hard dependency beyond the already-required permissions service.- **Maintainability and evolvability.** The `config` schema is versioned, so the filter vocabulary can change without breaking stored views: a reader migrates an older `config` version forward or applies it best-effort.
## Risks and Open Issues
- **Risk: stored `config` drifts from the dashboard's real fields.** As filters and columns evolve, older views reference fields that no longer exist. Mitigation: the versioned schema plus apply-what-resolves and the `stale_fields` signal (PRD FR-6). This is accepted, not eliminated.- **Risk: a shared-view permission leak.** A bug in the read-time permission re-check could expose data. Mitigation: the re-check is centralized in one code path, covered by tests that assert a reduced-permission recipient sees only permitted data, and gated behind a security review before sharing goes GA (PRD Phase 2).- **Open issue: team-owned default views.** Should a Team Lead be able to set a team default (PRD open question)? Not designed here; it raises ownership and override questions. Owner: Priya Nair (PM), before Phase 2.- **Open decision: the storage model belongs in an ADR.** As noted in Alternatives Considered, the dedicated-table choice should be recorded as its own decision record.- **Debt: no pagination on the views list at launch.** Acceptable while dashboards stay under ~200 views; tracked to add before that threshold is reached.Provenance
Section titled “Provenance”The reasoning, the history and every source, in the repository:
- Companion - the long-form argument: why these sections, where the sources disagree, and what the bundle refuses to claim
- History - what changed in this bundle, and when
- Research log - every source consulted, with what each one actually supports
- Catalog metadata - the machine-readable record this page is generated from
Catalog record: 11 sections across 1 format(s).