Write an architecture review

Write a design document for a multi-API system, with SLOs, a dependency inventory, capacity math, failure modes, data handling and decision records, plus a review checklist.

  • 45 min
  • Expert

An architecture review is where the work of this course becomes visible to someone else. Its job is not to prove the design is good. Its job is to put every assumption where a reviewer can disagree with it before production does: how many calls, how many credits, what happens when each dependency fails, where the personal data goes, and why each significant choice was made.

This lesson gives you a structure for that document, fills it in for a realistic system, and ends with the checklist a reviewer should run against any multi-API design, including yours. The worked example is a seller onboarding service for a marketplace: sellers sign up, verify their contact details and bank account, and are screened before they can receive payouts.

The shape of the document

Keep it short enough that people read it, and structured enough that missing sections are obvious. Seven sections cover what matters:

  1. Context. What the system does, for whom, and what is out of scope. Two paragraphs.
  2. Requirements and SLOs. What users are promised, as numbers.
  3. Dependency inventory. Every external API, with its cost, purpose, timeout and failure posture.
  4. Capacity estimate. Calls, credits and workers, derived from expected volume with the arithmetic shown.
  5. Failure-mode table. For each way each dependency can fail: detection, behaviour, user impact, recovery.
  6. Data handling. Which personal fields go where, how long they live, and what the audit trail records.
  7. Decision records. The choices that were contested or expensive to reverse, each with its alternatives and consequences.

A review then walks a checklist against those sections. The rest of this lesson takes them in order.

Context, requirements and SLOs

Context (worked example). Sellers register with an email, a phone number and a bank account (IBAN). Before activation we check that the email and phone are real and reachable, that the signup is not coming from an anonymising network, and that the IBAN is well-formed. Before any payout, the seller's legal name is screened against sanctions lists; active sellers are re-screened monthly. Out of scope: identity document verification and card payments.

Requirements. Write them as measurable promises, and write the ones about degradation as explicitly as the ones about success:

RequirementTarget
Registration response timep95 under 2.0 s, including all synchronous checks
Registration availability99.9% of submissions get a decision (activate, verify further, or reject)
Degraded registrationIf a contact check is down, registration still completes and the check is retried within 1 hour
Payout screeningNo payout to a seller without a clear screen newer than 35 days
Screening latencyNew sellers screened within 15 minutes of registration
Re-screenAll active sellers re-screened within 72 hours of the monthly run starting

Notice that "99.9% available" is defined as "gets a decision", not "every API answered". That is lesson one's posture work showing up as a requirement: a registration that completes with a deferred email check counts as served.

Dependency inventory

The inventory is the table reviewers return to most. One row per API, with the credit cost taken from the API's marketplace page and the fields sent taken from your outbound contract (lesson three):

APICreditsPurposeFields sentTimeoutPathPosture on failure
emailvalidator20Reachable, not disposableemail1.5 sSyncFail open, re-check queued
phonenumbervalidator10Valid, normalised to E.164number, country1.5 sSyncFail open, re-check queued
vpndetector10Anonymising network signalip1.5 sSyncFail to extra verification
ibanvalidator2Structural IBAN checkiban1.0 sSyncFail open; payout blocked until valid
sanctionscreening20Regulatory screenlegal name5 sAsyncFail closed: pending

Writing the table forces decisions that otherwise stay implicit. The IBAN check fails open at registration because a malformed IBAN cannot cause harm until a payout, and the payout path re-checks it. The sanctions screen is asynchronous because its posture is fail closed: putting it in the registration path would turn a screening outage into a registration outage, when all it needs to block is payouts.

Check what each call returns before relying on a field in the design. The phone check, for example, gives the normalised number you should store:

Try it livePhone Number Validator10 credits
GEThttps://api.apiverve.com/v1/phonenumbervalidator
curl "https://api.apiverve.com/v1/phonenumbervalidator?number=%2B14155552671" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

isValid is true and formatted.e164 is the canonical form to store. location and carrier are enrichment, not requirements.

Capacity estimate

Show the arithmetic, so a reviewer can change one assumption and see what moves. The estimate needs three answers: credits per month, peak calls per minute, and workers for each queue.

Assumptions for the worked example: 300 new sellers a day, 15% of a day's registrations in the busiest hour, 12,000 active sellers re-screened monthly, and a p95 latency of 1.5 s for the screening API.

capacity.mjs
const inventory = {
  emailvalidator: 20, phonenumbervalidator: 10, vpndetector: 10, ibanvalidator: 2, sanctionscreening: 20,
};
const perRegistration = ['emailvalidator', 'phonenumbervalidator', 'vpndetector', 'ibanvalidator', 'sanctionscreening'];

export function capacity({ newPerDay, peakHourShare, activeSellers, plan, p95Seconds, pacerShare = 0.9 }) {
  const creditsPerRegistration = perRegistration.reduce((sum, api) => sum + inventory[api], 0);
  const registrationCredits = newPerDay * 30 * creditsPerRegistration;
  const rescreenCredits = activeSellers * inventory.sanctionscreening;
  const monthlyCredits = registrationCredits + rescreenCredits;

  const peakCallsPerMinute = (newPerDay * peakHourShare * perRegistration.length) / 60;

  const pacedPerMinute = plan.perMinute * pacerShare;              // leave room for live traffic
  const rescreenMinutes = activeSellers / pacedPerMinute;
  const rescreenWorkers = Math.ceil((pacedPerMinute / 60) * p95Seconds); // Little's law

  return {
    creditsPerRegistration,
    monthlyCredits,
    shareOfAllowance: +(monthlyCredits / plan.monthlyCredits).toFixed(2),
    peakCallsPerMinute: +peakCallsPerMinute.toFixed(1),
    rescreenMinutes: Math.ceil(rescreenMinutes),
    rescreenWorkers,
  };
}

console.log(capacity({
  newPerDay: 300, peakHourShare: 0.15, activeSellers: 12_000, p95Seconds: 1.5,
  plan: { perMinute: 180, monthlyCredits: 1_000_000 },   // Pro, at the time of writing
}));
// { creditsPerRegistration: 62, monthlyCredits: 798000, shareOfAllowance: 0.8,
//   peakCallsPerMinute: 3.8, rescreenMinutes: 75, rescreenWorkers: 5 }

Read the output the way a reviewer would. Peak registration traffic is under 4 calls a minute, so the per-minute limit is irrelevant to the synchronous path. The monthly re-screen takes about 75 minutes with 5 workers, comfortably inside its 72-hour promise. The binding constraint is credits: the design uses 80% of the allowance before growth, retries of billable calls, or anyone else on the account. That finding is the whole value of the estimate. It turns "should we be on Pro?" from a feeling into a decision record, below.

Failure modes and data handling

Failure-mode table. One row per realistic failure, not per API. The columns that matter are how you detect it, what the system does, and what a user or operator sees:

FailureDetectionSystem behaviourUser impactRecovery
Email check slow or 5xxTimeout, breaker opens after 5 failuresSkip check, flag emailUnverified, queue re-checkNone at signupRe-check worker drains queue when breaker closes
Screening API downTimeout or breaker openSellers stay pending; payouts heldPayout delayed; banner explainsScreens resume; oldest-pending alert after 15 min
Rate-limit 429Status plus credits header above 0Pause the pool for retry-afterNone; queue absorbs itAutomatic
Credits exhausted429 and x-api-remaining-credits is 0Stop all dispatch, page on-callContact checks deferred, payouts heldUpgrade or renewal; then drain
Bad input (400)StatusReturn field error to the form; do not retryUser corrects inputNone needed
Re-screen run overrunsOldest-job age above 48 hRaise re-screen priority, alertNone yetInvestigate before 72 h promise breaks

Data handling. Reference the outbound contract rather than repeating it, then state what lesson three asked for: where each personal field is stored, the lifetime each store enforces, how deletion reaches caches and logs (pseudonymous tokens), and what the audit trail holds. For this design: projected results cached for 30 days keyed by HMAC tokens; operational logs redacted by field name and kept 14 days; sanctions audit entries append-only and hash-chained, with a retention period set by counsel; the processor list (every API above) and their published data terms linked from the document.

Check yourself

In the worked capacity estimate, peak registration traffic is under 4 calls per minute on a 180 per minute plan, and monthly usage is 80% of the credit allowance. What should the review conclude?

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales