Logical design & articulating tradeoffs
The whiteboard moment — how to go from requirements to a diagram an interviewer can attack.
Concept → your projects
After requirements (Day 1) comes the logical design: API surface, data model, and the components that transform one into the other. The method that survives interviewer attack:
- Draw the entities first (the data model is the system's center of gravity).
- Name the flows (write path, read path, async path) — one sequence per flow.
- Mark every box with its consistency/availability role (source of truth? cache? queue? projection?).
- Say the tradeoffs out loud as you draw — a silent diagram is an unexamined one.
The tradeoff articulation template
Memorize this sentence shape and you can improvise on any topic:
"We need [NFR]; option A gives it via [mechanism] at the cost of [downside]; option B avoids that cost but weakens [other NFR]; given [constraint] I chose [A/B]; the upgrade path is [X]."
Run it on three of your real decisions:
- Durability of provisioning events: "We need access changes to eventually reach GitHub even if the broker dies. Option A — publish directly to RabbitMQ — risks losing the event on broker unavailability (the API tolerates broker-down at boot, so this is a real window). Option B — transactional outbox — persists the event in the same Postgres transaction as the state change and relays later, costing a 5 s polling delay and an extra table. I chose B because in IAM, a lost revoke is a security incident; a 5-second delay is nothing."
- Approval latency: "Human-paced, so synchronous email-free in-app approval keeps ACID simplicity; the alternative (event-sourced approvals) buys replay at the cost of read models — unnecessary here."
- Read scaling in traverse: "Stats reads are fan-out heavy per user; cache-aside Redis with 120–600 s TTLs bounds staleness; write-through would cost double writes on every submission for freshness nobody needs."
Your two logical designs, condensed to memory
Zoth (the control plane):
clients ──HTTP/JWT──► api ──SQL──► Postgres (system of record: requests, │ approvals, policies, audit, outbox, │ pgvector docs) ▲ ▲ │ outbox relay (5s) │ │ direct reads ▼ │ │ RabbitMQ (zoth.access topic) │ │ │ │ │ ▼ │ │ emitter ──zlink──► GitHub/Azure/Notion alerting ──5min tick──► CompareDrift ──► audit + drift events
Roles: Postgres = source of truth; RabbitMQ = transport (lossy-tolerant because outbox); emitter = side-effect executor; alerting = reconciliation loop. Every component's role is nameable — that's what makes the design defensible.
Traverse (the data plane):
extension ──POST──► api ──► Postgres (Submissions, Solves, Revisions, │ Streaks, Rings, Notifications) = SSOT └─enqueue► Redis/BullMQ ──► workers: AI, FSRS, streaks, achievements reads: api ─► Redis cache (TTL) ─► Postgres
Same discipline, inverted emphasis: zoth's async path exists for safety (don't lose revokes), traverse's for latency (don't block on LLMs).
Handling the attack
When an interviewer pokes a box, the answer is always about roles and failure modes, never "that's how it's usually done":
- "What if the emitter is down when access is granted?" → Event sits in the durable queue; consumer retries up to 5 times with republish-based backoff, then DLQ (
emitter.access.dlq); on restart, consumption resumes. Drift detection additionally reconciles within 5 min as a safety net. Bounded eventual consistency. - "What if two approvals race?" →
UpdateStatusConditional—UPDATE ... WHERE status='PENDING'; loser getsErrConflict(409). The conditional update is the concurrency contract (Day 32). - "What if Redis dies in traverse?" → Cache ops degrade to no-op (2 s timeouts), API serves from Postgres; queues lose pending jobs — accepted because submissions themselves are already committed and the extension retries; BullMQ's at-least-once redelivery covers the rest.
A drill
Take "design a paste service" (10-min version): entities (paste, user), write path (create → id → store), read path (get by id, cache-aside), NFRs (pastes immutable → cacheable forever; TTL-based deletion), tradeoffs (counter-based IDs vs base62 random; storage on disk vs DB vs object store). Then say one upgrade path per choice. That's a complete logical design — notice how little drawing it actually needs.
Bulletproof your resume
Say these answers aloud before moving on
These are the follow-ups an interviewer will actually ask about your projects.
Draw zoth's architecture and justify each component in one breath.
Postgres is the system of record — policies, requests, approvals, audit, and the outbox live in one ACID store, so state transitions and event emission commit atomically. The API is a stateless Chi service holding only request-scoped state — JWTs make it horizontally scalable the day I add replicas. RabbitMQ is a transport, not a source of truth — the outbox means losing it delays provisioning but never corrupts state. The emitter isolates the risky external-API work with retries and a DLQ. Alerting closes the loop: desired state (DB) vs actual state (platforms) every 5 minutes, because external systems can drift independent of my events.
What's the weakest box in your diagram?
Single-replica RabbitMQ and the 10,000-row cap in audit VerifyChain, but the design weakness I'd name first: alerting's drift detection and notification sending are not fully wired to each other yet — drift writes audit rows and events, but in-app notifications aren't emitted from that path. The fix is small (call notification service from drift handlers), and saying it precisely is better than hoping nobody asks.
How would you explain your architecture to a non-technical stakeholder?
Zoth is the approval desk for keys: you ask for a key, your manager's desk approves it, the key works for exactly the time granted, expires automatically, and every step is written in a tamper-evident ledger. The plumbing underneath — queues, connectors — exists so the desk keeps working when the systems it grants keys to (GitHub, Azure) are slow or down.