ADD 3.0: A Worked Example of the Seven-Step Design Method

By Arkaitz

You have sat in the review. Forty boxes on a diagram, six arrows between them, and one question nobody can answer: which requirement forced this queue to exist?

The diagram survived because it looked finished. The reasoning did not, because nobody wrote it down. Six months later the queue is the thing you cannot remove, and no one remembers what it was protecting.

Attribute-Driven Design 3.0 (ADD 3.0) is a repeatable loop that turns architectural drivers into structures and records why. It does not produce the diagram first. It produces the trail first, and the diagram falls out of it.

This post walks one system through three iterations: the inputs, the seven steps, the decisions, and the analysis that says whether it worked. The example is a composite, not a client, and every artifact below is one you can copy.

If you are still deciding how much process your team needs, read how to make better architecture decisions first. This post assumes the decision is to design. The wider sequence is in the system design practical guide.

What is ADD 3.0, and why does it exist?

ADD 3.0 is an architecture design method from the Software Engineering Institute. Each iteration is driven by a small set of architectural drivers. Humberto Cervantes and Rick Kazman introduced the method in a 2015 SEI tutorial and published it in Designing Software Architectures: A Practical Approach in 2016.

The lineage matters. SEI published ADD 1.0 in January 2000 and ADD 2.0 in November 2006. ADD 2.0, in the SEI's own words, focused on a conceptual architecture design and promoted the use of Patterns and Tactics as key concepts that drive the design process.

That focus was also the limitation. ADD 2.0 was written before agile methods were widespread, so it says little about designing in small increments. It also treated patterns and tactics as the main concepts, while practitioners reach for reference architectures and existing frameworks.

ADD 3.0 widens the toolbox and makes the iteration explicit. The core definition is simpler than the reputation: Designing means making decisions to achieve goals and satisfy requirements and constraints. The method is the discipline that keeps those decisions tied to the goals instead of to taste.

What counts as an architectural driver in ADD 3.0?

An architectural driver is any input that shapes the architecture and matters enough to defend. ADD 3.0 names five kinds:

Driver type What it answers Example
Design purpose Why are we designing now? Launch a paid v1 in four months
Quality attributes How well must it behave? p95 PDF render under 800 ms
Primary functionality What must it do first? Create, send, and reconcile invoices
Architectural concerns What must we handle across the system? Audit trail, tenant isolation, observability
Constraints What is fixed? One region, one managed Postgres, no Kubernetes

Two of these carry most of the weight, and both are usually written badly. Design purpose is often missing because everyone assumes it. Quality attributes are often adjectives: fast, secure, scalable. An adjective cannot be tested, so it cannot drive a decision.

That is why ADD 3.0 leans on the quality attribute scenario. A scenario has six parts: source of stimulus, stimulus, environment, artifact, response, and response measure. Bass, Clements, and Kazman define the form in Software Architecture in Practice, and the response measure turns a wish into a requirement.

Here is a weak driver and the same driver rewritten:

  • Weak: Payments must be reliable.
  • Scenario: When the payment provider retries a payment.succeeded webhook for up to 24 hours during a provider degradation, the payment intake marks each invoice paid exactly once, with zero duplicate paid invoices and no lost events.

Now the driver can fail, which is the point.

The book is strict about what counts as architectural. Its test is consequence, not size: A decision is architectural if it has non-local consequences and those consequences matter to the achievement of an architectural driver. Renaming a variable fails that test. Deciding where invoice state lives passes it, because six other things read that state.

Cervantes and Kazman also write that the architect's job is not one of finding an optimal solution, but rather one of satisficing — good enough against your selected drivers, with the trade-offs written down.

One rule before moving on: a driver without a response measure is an opinion. If you cannot state how you would know it was met, it belongs in a backlog, not in this iteration.

How do the seven steps of ADD 3.0 work?

ADD 3.0 has seven steps, and every one of them has to leave an artifact behind: if a step produces nothing you can read, the step did not happen. Here they are, with the artifact each one owes you:

Step Name Artifact
1 Review inputs Driver sheet and scenario table
2 Establish the iteration goal by selecting drivers One sentence naming the goal and the drivers it serves
3 Choose one or more elements of the system to refine Named element, and why this one
4 Choose one or more design concepts that satisfy the selected drivers Two or more candidates, with rejected reasons
5 Instantiate architectural elements, allocate responsibilities, define interfaces Element and responsibility table, interface sketches
6 Sketch views and record design decisions Views plus decision records
7 Perform analysis of the current design and review the iteration goal and achievement of design purpose Driver-by-driver analysis, residual risks

You will notice what is missing: a step for draw the architecture. The views in step 6 are a by-product of decisions already made. The method expects you to iterate, and the book gives different roadmaps for greenfield systems in mature domains, greenfield systems in novel domains, and brownfield systems.

Brownfield is the roadmap most readers will use, and it changes iteration 1 more than anything else in the method. In a new system, step 3 refines the whole system because nothing else exists. In a running system, step 1 already has inputs you did not write: the code, the incident history, and the drivers the current design quietly serves.

Step 3 then selects one element of the live system, and step 4 carries change nothing as a real candidate. The book's §3.3 treats brownfield as a different starting point: you begin from the existing documentation and the new drivers that broke it, so the first iteration is usually a repair.

What does one ADD 3.0 iteration look like on a realistic system?

The system is Ledgerline: a multi-tenant invoicing and payments product for small B2B SaaS companies. The constraint that shapes every choice below is a team of three engineers with Rails and Postgres skills, four months to a paid launch, and a €600-a-month infrastructure ceiling.

That ceiling rules out every architecture that assumes a platform team. What ecommerce architecture looks like at 100 users hits the same wall from the traffic side.

Step 1 produces the driver sheet. This is the only artifact that must be complete before design starts.

ID Type Driver Measure
DP-1 Purpose Paid v1 in four months with three engineers Launch date
QA-1 Availability Provider webhook retries for 24h; mark paid exactly once Zero duplicate paid invoices; no lost events
QA-2 Performance Open invoice list; export a 200-line PDF p95 list under 500 ms; PDF under 800 ms at 50 concurrent
QA-3 Modifiability Add a second payment provider Five engineer-days; one module touched
QA-4 Security Card data never touches our servers SAQ A; no PAN in logs or database
CR-1 Constraint EU data residency, single region Fixed
CR-2 Constraint One managed Postgres, no Kubernetes Fixed
AC-1 Concern Every invoice state change is attributable Audit entry per transition

Ledgerline is a greenfield system in a mature domain, so the roadmap starts from a reference architecture. That saves a week of debate about inventing a new structure.

Iteration 1: establish the overall structure

Step 2, iteration goal: produce an overall system structure that supports the primary functionality and both constraints without violating the security driver. Drivers selected: DP-1, CR-1, CR-2, QA-4, AC-1.

Step 3, element to refine: the whole system. There is nothing else yet.

Step 4, design concepts. Candidates, not conclusions:

  1. Modular monolith with bounded contexts, server-rendered, managed Postgres.
  2. Microservices per domain, deployed independently.
  3. Serverless functions per endpoint with a managed database.

The third candidate dies on CR-2 and DP-1: the team would spend the launch window learning a deployment model they do not have. The second dies on DP-1 and QA-3: three engineers cannot operate five services and still ship in four months. The first survives because it concentrates complexity in one deployable unit while keeping module boundaries enforceable.

That is a decision with a named cost, and the modular monolith trade-off is where the cost gets paid. Splitting later stays a real option, and the distributed system sequence covers when the split stops being optional.

Step 5, instantiate and allocate. The system becomes five modules with explicit responsibilities:

Module Responsibility Interface
Identity Tenants, users, sessions Current.tenant, sign-in
Invoicing Invoice lifecycle, line items, PDF requests InvoiceService
Payments Provider webhooks, payment state, reconciliation PaymentGateway port
Billing Plans, subscriptions, usage BillingService
Notifications Email and in-app notices Notifier port

Two ports exist for one reason each. PaymentGateway isolates the provider behind a contract, which is the only design move that makes QA-3 plausible. Notifier keeps delivery out of the invoice transaction.

A port is not an interface until you write the methods down. Here is the sketch that goes in the iteration record:

# app/ports/payment_gateway.rb
class PaymentGateway
  # in: our invoice reference, amount in minor units, currency, our idempotency key
  # out: ChargeResult(reference:, status:, raw:)
  # raises: Gateway::Declined | Gateway::Unavailable
  def charge(invoice_ref:, amount_cents:, currency:, idempotency_key:); end

  # in: provider charge reference. out: ChargeResult with current status
  # raises: Gateway::Unavailable | Gateway::NotFound
  def fetch(charge_ref:); end

  # in: raw body, signature header. out: Event(kind:, provider_event_id:, charge_ref:, occurred_at:)
  # raises: Gateway::InvalidSignature
  def parse_webhook(body:, signature:); end
end

Three methods, four error types, one rule: no provider SDK exception escapes the adapter. Declined is a business outcome and gets recorded. NotFound sends the charge to reconciliation, which decides whether the capture was missed or the reference is stale. Unavailable is retried, and InvalidSignature stops the request at the edge.

That error contract is the point of the port: a second provider arrives as a new adapter, not a new branch in every controller. The idempotency key is ours, derived from the invoice and the payment attempt; the provider's event id is inbound and belongs to the webhook path.

Step 6, sketch and record. The container view is boring on purpose: browser, one Rails application, one managed Postgres, one object-storage bucket, and one background worker process. The decisions carry the weight.

ID Decision Rationale Rejected alternative
D1 Modular monolith, one deployable Team size and launch date; boundaries enforced by tests Microservices per domain
D2 Managed Postgres as the system of record Transactions for invoice state and audit Event store, separate read model
D3 PaymentGateway port and adapter QA-3: second provider is an adapter, not a rewrite Provider SDK called directly from controllers
D4 Server-rendered pages DP-1: no separate frontend to build or operate SPA plus API
D5 PDFs in object storage Keeps large blobs out of Postgres; immutable artifacts PDF bytes in a database column

D1 is the decision most likely to be re-litigated in month five, and the deployment-unit trade-off is the honest ledger for it. One deployable is cheap until it is not.

Step 7, analyze. The structure satisfies DP-1, CR-1, CR-2, QA-4, and AC-1. It does not satisfy QA-1, because webhook handling is still just a controller. It does not satisfy QA-2, because PDF rendering is synchronous inside the request and the invoice list has no paging plan.

Neither QA-1 nor QA-2 was selected for this iteration, so their absence is not a contradiction. Both go to the top of the backlog.

Iteration goal: met. Design purpose: still on track.

New risks: the payment path and the PDF path. They become the next iterations, in that order. You do not fix them in this round.

Iteration 2: make payment intake exactly-once

Step 2, iteration goal: satisfy QA-1 under duplicate and delayed webhook delivery. Driver selected: QA-1.

Step 3, element to refine: the Payments module.

Step 4, design concepts and tactics. Four candidates:

  1. Verify the signature, insert the raw event into a table with a unique key on (provider, event_id), write an outbox row in the same transaction, and let a worker apply the state change.
  2. Enqueue the event, return 200, and process asynchronously with at-least-once delivery.
  3. Process the webhook synchronously and reply after the invoice update commits.
  4. Trust the provider's dashboard and reconcile nightly.

Option 4 is tempting, because the provider's dashboard already shows the truth. It fails three ways: you hold the wrong invoice state for up to 24 hours, you have no exactly-once guarantee, and someone works a conflict queue every morning. Reconciliation compares after the fact; it cannot carry the guarantee.

Option 3 couples the provider's timeout to your database transaction and still duplicates on retry. Option 2 loses events when the process dies between the 200 and the enqueue, and duplicates when the provider retries. Option 1 costs one table, one outbox, and one poller, and survives both.

The useful part of option 4 survives inside option 1 as a nightly reconciliation job. It prevents nothing. It tells you whether the design is lying.

Step 5, instantiate. The flow becomes:

  1. Verify the webhook signature; reject on failure.
  2. Insert a payment_events row, unique on (provider, event_id). On conflict, stop: the event was already accepted.
  3. If the insert succeeded, write an outbox row in the same transaction.
  4. A worker polls unprocessed outbox rows, applies the invoice transition, writes the audit entry, and marks the outbox row processed.
  5. Failures retry with exponential backoff; after five attempts the outbox row moves to payment_dead_letter and an alert fires.

Three tables, three jobs. payment_events holds what the provider sent, outbox holds what we intend to do about it, and payment_dead_letter holds what we could not do. The worker only updates the last two.

The crucial detail is step 3. The database write and the intent to process commit together, so a crash cannot leave a stored event with no worker. That is the transactional outbox, and it is the smallest mechanism that makes the guarantee real.

Step 6, record. D6: idempotency key on the provider event. D7: transactional outbox instead of a direct job enqueue. D8: dead-letter after five attempts, with an alert on unprocessed outbox rows older than 60 seconds.

Step 7, analyze. Duplicate delivery is absorbed by the unique key. A crash between the database commit and the worker poll is absorbed by the outbox. A provider retry after a successful transition hits the same unique key and no-ops.

That is an argument about the design, not a test result. The falsification path is short: replay one webhook body five times and count paid invoices, kill the worker between commit and poll and check the invoice still gets paid, post an unsigned body and expect a rejection. If any of those produces two paid invoices or a lost event, the design is wrong. None of it has run yet.

Residual risk: the poller is one process, so its failure delays processing. The alert on outbox age is the detection, and a second poller is the upgrade.

Iteration 3: cut PDF latency without lying to yourself

Step 2, iteration goal: satisfy both QA-2 measures — list under 500 ms, PDF under 800 ms at 50 concurrent — without adding infrastructure. Drivers selected: QA-2, DP-1.

Step 3, element: two of them, the render path and the list read path. One iteration can carry two elements when they share a cause: both read the invoice table on the request path with no plan.

Step 4, concepts. For the PDF: render synchronously with a version-keyed cache, render asynchronously for every request, pre-render on issue, or move rendering to a separate service. The last is premature for three engineers, and pre-rendering fails for invoices that change after issue. Async-for-everything fails the open and read now experience. The chosen combination is synchronous render on demand, an immutable cache keyed by invoice_id + version, and bulk exports queued off the request path.

For the list: offset pagination with a COUNT(*), keyset pagination with a count-free pager, or a materialized view per status. The view adds a refresh job and stale rows to explain. Offset with a count is the ORM default, and it is the one that falls over.

Step 5, instantiate and allocate. Two elements, three responsibilities:

Element Responsibility Interface
Render path Render one invoice version to PDF and cache the artifact Renderer.render(invoice_id:, version:) -> RenderResult
Invoice list Serve one page of invoices by keyset cursor InvoiceList.page(tenant:, status:, cursor:, limit:) -> Page(items:, next_cursor:)
Export job Queue and run bulk exports off the request path ExportJob.enqueue(tenant_id:, invoice_ids:) -> job_id

RenderResult is either {bytes, page_count} or :render_failed. A cache miss is the only slow path, and the interface makes that visible instead of hiding it behind a boolean.

The list path. The list is one query with one index and no count. It returns a tenant's invoices, filtered by status, ordered by due date:

SELECT id, number, status, due_date, total_cents
FROM invoices
WHERE tenant_id = $1 AND status = $2 AND (due_date, id) > ($3, $4)
ORDER BY due_date, id
LIMIT 51;

The index that matches it:

CREATE INDEX invoices_tenant_status_due_idx
ON invoices (tenant_id, status, due_date, id)
INCLUDE (number, total_cents);

The column order is the design. The two equality predicates come first, so tenant_id and status can be in either order among themselves. Everything after them has to follow the ORDER BY, or Postgres adds a sort node and the index stops paying for itself.

The trailing id is the tiebreak that keeps the cursor stable when two invoices share a due date. The INCLUDE columns make the scan index-only, so the heap is touched only for rows you return.

The cursor is the last row's (due_date, id), not an offset. OFFSET 40000 makes the database walk forty thousand rows and throw them away, so page one is fast and page eight hundred is an incident. A keyset cursor costs the same on both.

The pager has no total. It fetches 51 rows, renders 50, and shows next if the fifty-first exists. You lose page 37 of 10,000 and jump-to-end, which nobody uses and which costs a full scan.

What gets measured is p95 for the first page and p95 for a deep page, on a tenant with roughly 500,000 invoices, from EXPLAIN (ANALYZE, BUFFERS) and from request timing. The plan should show an index-only scan with buffer hits, not a sequential scan and not a sort. That measurement is the falsification: over 500 ms on either page, or a sort node, means the design failed. The whole change stays small: one index, one query, one pager, no search cluster.

Step 6, record. D9: cache immutable PDFs in object storage, keyed by invoice_id + version. D10: bulk exports become jobs, not requests, queued above 50 invoices. D11: bulk rendering runs in the worker's render pool, capped at three concurrent renders and separate from the Rails request processes, so one of the four cores stays free for interactive traffic. D12: no COUNT(*) on the list; keyset pagination over a covering index on (tenant_id, status, due_date, id).

D11 needs stating plainly, because cap the pool is not a concurrency model. There are two pools and they do not share a limit. Interactive renders run inside the Puma request, bounded by Puma's thread count. Bulk exports run in the worker's render pool of three, and the reserved capacity is the fourth core, which the worker never touches.

Step 7, analyze. The cache improves p95 and introduces a freshness question. The design answers it structurally: invoices are versioned, and a version never changes. Editing an invoice creates a new version, hence a new cache key. The list has no such question, because the index reads the same rows the page shows.

The PDF trade-off is storage growth, which is cheaper than a rendering incident. Residual risk: the cold-cache render of a 200-line invoice at 50 concurrent, with a load test against the production template as the falsification. On the list side, the risk is a tenant whose status distribution makes one filter path much hotter; the falsification is the same measurement against the largest real tenant.

Iteration 4 would prove QA-3 by adding a second provider adapter behind PaymentGateway and running the same contract tests — build the second implementation, not a slide about it.

The container view after three iterations is one clause longer: browser, one Rails application, one managed Postgres, one object-storage bucket, one worker process running the outbox poller and the capped render pool. Nothing else was added, and that is the point of the iteration goal.

How do you know the iteration achieved its goal?

You know because every driver the design claims to satisfy has a stated way to be proven wrong, and the analysis names the measurement that would do the proving. Step 7 is an argument, driver by driver, not a status update.

The distinction that keeps this honest is design time versus run time. At the end of an iteration you have a structure, a set of decisions, and for each driver a falsification path: the test, replay, or load run that would show the design misses its measure. Naming that path is design work, and it is the part teams skip.

Driver How the design addresses it How it would be falsified Residual risk
DP-1 Modular monolith, server-rendered, existing stack, no new platform Compare planned scope against the four-month date at each iteration; a slip past two weeks falsifies the structure Team of three; scope creep
CR-1 Single EU region, managed Postgres in the EU Deployment config audit and data-residency check; any non-EU service or backup falsifies it Provider sub-processors outside the EU
CR-2 One managed Postgres, no Kubernetes, one deployable Infrastructure review; a second datastore or an orchestrator falsifies it Managed Postgres connection limits
QA-1 Unique payment_events key, outbox in the same transaction, idempotent worker Replay one webhook five times; kill the worker between commit and poll; post an unsigned body Poller is a single process
QA-2 Versioned PDF cache, separate capped render pool, keyset list over a covering index Load test at 50 concurrent against the real template; deep-page p95 against the largest tenant Cold-cache p95 unknown; hot status filter untested
QA-4 Provider-hosted card fields; no PAN storage Code review, log scan, and a browser check for third-party scripts Third-party script on checkout
AC-1 Invoice transitions write an audit row in the same transaction Attempt a transition without an audit row; the invariant test fails Audit table growth

If a row cannot name how it would be falsified, the iteration is not done. It is designed, which is a different state. A design with no falsification path is a claim, and claims are what this method exists to replace.

For a heavier check, run an ATAM-style review against the same drivers. ATAM is an evaluation method, not a design method: it does not produce the architecture, it stress-tests yours.

The SEI's ATAM collection describes the full process. For a three-person team, a 90-minute version with the driver table and three scenarios captures most of the value.

ADD 3.0 vs ADD 2.0 vs ATAM: what is the difference?

Method Question it answers Output When to use it
ADD 2.0 What structure satisfies the driving quality attributes? Conceptual architecture from patterns and tactics Legacy contexts; the method assumes upfront conceptual design
ADD 3.0 What structure satisfies this iteration's drivers, and why? Iteration-sized structures, decisions, and analysis Greenfield, novel, and brownfield design where you need a trail
ATAM Where does this architecture fail or trade off badly? Risks, sensitivities, trade-off points After a design exists and the cost of being wrong is high

ADD 3.0's real change is not the step count. It is that the iteration goal is explicit. ADD 2.0 asked you to identify architectural drivers and decompose. ADD 3.0 asks you to choose which drivers this round serves, which forces you to admit what you are not designing yet.

That is also where most teams get it wrong. They treat the seven steps as a checklist for a single big design, produce forty boxes, and learn nothing. The method is a loop, and each loop should finish with a falsification path.

Where does ADD 3.0 stop working?

ADD 3.0 is not a stakeholder discovery method. It assumes the drivers exist and are agreed. If you cannot get the product owner, the security lead, and the person paying for infrastructure in one room, you are designing against guesses. The SEI's Quality Attribute Workshop fills that gap before ADD starts.

It is also not a substitute for code. The output is a conceptual architecture with interfaces and responsibilities. Performance, availability, and security claims stay hypotheses until a prototype or a load test touches them. The method tells you which hypothesis to test first; it does not test it for you.

And it can become ceremony. Fill every cell of the matrix — seven steps, eight drivers, three elements — and you produce 168 answers nobody reads. Timebox an iteration to 90 minutes, keep the driver sheet to a page, and write the decision record in the session. If an iteration cannot name a falsification path, shrink it.

The method is weakest in novel domains, where no reference architecture exists and the design concepts are guesses. There, the roadmap expects prototypes and spikes before commitment, and those cost time you may not have. Use ADD 3.0 to structure the uncertainty, not to pretend it away.

Finally, do not confuse the design record with the decision record. ADD's step 6 gives you the reasoning inside an iteration. The durable, one-decision-per-file artifact is an architecture decision record, and it should link back to the iteration rather than duplicate it.

What does a one-page ADD 3.0 iteration look like?

One page, filled in order, with a blank line treated as a blocker. Copy this into your design doc and work down it.

Iteration: ___
Goal (one sentence): ___
Drivers served: ___ (IDs)
Element(s) refined: ___

Concepts considered:
1. ___  (accepted / rejected because ___)
2. ___  (accepted / rejected because ___)

Responsibilities and interfaces:
- Element: ___  Responsibility: ___  Interface: ___

Decisions:
- D1: ___  Rationale: ___  Rejected: ___

Analysis:
- Driver ___: addressed by ___  Falsified by ___  Residual risk: ___
- Goal met? ___   Design purpose still served? ___

Next iteration: ___

The first iteration on a new system usually takes a day. Later iterations take 90 minutes. The cost is not the meeting; it is naming the driver before the solution.

If you run more than a few iterations, the book's §3.8 gives you the tracking device: an architectural backlog of drivers and concerns, pulled across a design Kanban board instead of assigned in a planning meeting. The board is not project management. It makes a driver nobody is designing this month visible.

The design is the trail, not the diagram

Your container diagram will be stale in a quarter. The queue will move, the modules will split, and the provider will change. What survives is the record of which driver forced each structure and which alternative you rejected.

That record is what lets the next engineer change the system without reversing a decision they cannot see. It is also what lets you defend the design in a review without reconstructing the argument from memory.

Run one iteration this week. Pick the driver that scares you most, write the scenario with a response measure, and follow the seven steps until you can state what would prove the design wrong. The diagram can wait. The reasoning cannot.

Related Reading