High-Level System Design Methodology
TechLearningPro's growth squad was asked, on a Thursday, to "sketch the architecture" for public lesson permalinks: a short code a learner can paste into a cohort chat that opens t
High-Level System Design Methodology
Advanced System Design & Software Architecture · Lesson 23 · High-Level Design and Distributed Systems
Lesson Overview
TechLearningPro's growth squad was asked, on a Thursday, to "sketch the architecture" for public lesson permalinks: a short code a learner can paste into a cohort chat that opens the right lesson. By Friday the whiteboard had a gateway, two caches, a message bus, a key-value store, and a second region. Nobody had written who was allowed to create a link, what the redirect was allowed to point at, or what should happen when an instructor revoked a lesson. The diagram was a catalog of mechanisms. The product question was still open.
High-level design is a sequence that keeps mechanisms from arriving before the claim they serve. The sequence in this lesson is the one a design review and a system-design interview can both follow without changing the standard of evidence: clarify the behavior, write the quality scenarios and the constraints, estimate the workload well enough to see which resource moves, define the APIs, assign data ownership, name the components, then stress the sketch with scale and with failure. The artifact at the end is a short decision record, not a poster.
The primers at /learn/high-level-design/home and /learn/system-design/interview-framework introduce the vocabulary and a clock-friendly outline. This lesson is stricter. Every number is labeled as a measurement or an assumption. Every box has a data owner. Every failure has a user-visible response. The record uses the ADR shape from /learn/advanced-system-design/architecture-documentation-and-adrs. Estimation arithmetic is lesson 24. Scaling laws are lesson 25. Load balancers, caches, and the full permalink case study come later; here they appear only when the sequence reaches them, and only as decisions with a tax.
You will apply the sequence to TechLearningPro lesson permalinks and leave a packet a staff engineer can reject.
Learning Objectives
By the end of this lesson you will be able to:
- Run a high-level design in a fixed order: behavior, scenarios, estimates, APIs, data ownership, components, scale path, failure modes, decision record.
- Separate functional behavior, quality-attribute scenarios, constraints, and assumptions, and mark which numbers are still guesses.
- Write an API surface that states success, absence, and revocation, including the trust decision about where a redirect may send a browser.
- Assign each stored fact to one owner and say what a cache is allowed to answer.
- Name the first saturated element under a stated multiplier, and the user-visible behavior when a dependency is down, slow, or wrong.
- Produce an architecture decision record whose consequences include the option you refused and the condition that reopens it.
- Review someone else's high-level design by asking which step was skipped, rather than by adding boxes.
Prerequisites
- Lessons 1 through 3: structures, six-part quality scenarios, and architecture drivers. This lesson consumes scenarios. It does not replace them.
- Lesson 10: an ADR has status, context, options, a decision, and consequences. You will write one record, not a documentation system.
- Comfort reading an HTTP status line and a small Spring MVC controller. The code in this lesson uses
ResponseEntity,UriComponentsBuilder, and the JDKURIclass. It does not introduce a new web framework. - The capacity formulas in lesson 24. This lesson only needs estimates accurate to an order of magnitude, with the arithmetic shown.
- The primer courses are optional context. If you have already read /learn/high-level-design/requirements or /learn/system-design/requirements-clarification, treat them as vocabulary. The deliverable here is the packet and the failure analysis.
Detailed Core Concepts
What a high-level design is responsible for
A high-level design answers questions a single class cannot:
- Which behaviors are in scope for this iteration, and which are named and deferred?
- Which quality scenarios can fail the design?
- Which workload numbers, even rough ones, change the shape?
- What does a caller send, and what do they receive on success, on absence, and on conflict?
- Which component is allowed to decide a fact, and which components hold disposable copies?
- What breaks first when the workload grows by the multiplier you wrote down?
- What does the user see when a dependency fails, and what data is left behind?
A class diagram of the redirect controller does not answer these. A deployment diagram of every technology the team has used does not answer them either. High-level design sits between the brief and the module design. Low-level design, which module 2 practiced on parking, elevators, and payments, assigns responsibilities to types and protects invariants in memory. High-level design assigns responsibilities to deployable pieces and stores, and protects invariants that survive a process restart.
The interview version and the staff-review version are the same sequence with different time boxes. An interview compresses the estimates and expects you to say "assumption." A review expects you to replace the assumption with a measurement or to keep the label. Neither version rewards a diagram that cannot name a failure.
The sequence, and why the order is the method
The order is the part candidates skip. Each step produces an input the next step is not allowed to invent silently.
- Clarify behavior. Actors, the jobs they do, and an explicit out-of-scope list. If you cannot say what the system refuses to do, every later box is speculative.
- Scenarios and constraints. A few six-part scenarios from lesson 2, plus constraints you did not choose: data residency, an existing identity provider, a team size, a budget, a compliance boundary.
- Estimates. Daily active users, reads, writes, payload sizes, retention, peak shape. Lesson 24 is the full method. Here you compute enough to see whether the design is a single database, a cache in front of it, or a network problem. You label every input.
- APIs. The operations a caller can perform, the identifiers, the status outcomes, and the authorization decision. An API that only documents the happy path hides the failure design.
- Data ownership. Entities, keys, who writes, who may serve a stale read, and the retention rule. One fact has one writer.
- Components. The smallest set of runtime pieces that own those facts and expose those APIs. A box earns its place by owning data or by terminating a trust boundary.
- Scale path. The element that saturates first under the multiplier, and the next tactic you would apply. You do not apply every tactic in the first sketch.
- Failure modes. For each dependency and each important write: detection, user-visible response, and the state left behind. This is where most interview diagrams stop too early.
- Decision record. One or two decisions that were actually close, with the rejected option and a reopen condition.
Stopping after step 6 produces a picture. Steps 7 through 9 are what make it a design. Reversing the order — drawing the bus first — is how the Thursday whiteboard happened.
Behavior, scenarios, constraints, assumptions
These four words get collapsed into "requirements" and then ignored. Keep them apart.
Behavior is what a named actor can do. "An enrolled learner opens a permalink and lands on the lesson." "An instructor revokes a permalink and later opens receive a gone response." Behavior is testable without a latency number.
Scenarios attach a measure and an environment to a behavior, using the six slots from lesson 2. A permalink design needs at least a read-latency scenario, a revocation scenario, and an authorization scenario. Three is enough for one iteration. A list of twenty adjectives is not a list of scenarios.
Constraints are conditions you do not get to vote away in the meeting: the identity provider is the one the company already runs; enterprise tenants cannot place lesson content on a public CDN; card data stays at the payment vendor, as lesson 1 already decided. Constraints remove options. Write them down so a later diagram does not quietly redraw them.
Assumptions are numbers and facts you are proceeding with because nobody has measured them yet. "Ten million permalink resolutions a day" is an assumption until analytics says otherwise. Assumptions are allowed. Unlabeled assumptions are how a design becomes fiction. The packet has a section titled assumptions, and every figure in the estimates points back to it.
A useful clarification question changes scope, a measure, or an owner. "Can a permalink target any URL on the internet, or only a lesson we host?" changes the security design. "Is a revoked link allowed to work for another minute?" changes the cache. "Do enterprise lessons leave our region?" changes the deployment. "Should we use Kafka?" does not clarify a requirement. It proposes a mechanism before step 6.
Estimates as a filter, not as a performance
Order-of-magnitude arithmetic is part of the method because it deletes boxes. If permalink resolution is a few hundred requests per second at peak, and each row is a few hundred bytes, a single relational primary with a replica is a credible store. A sharded key-value cluster is a mechanism you have not earned. If the same product later stores arbitrary marketing URLs at a hundred times the rate, you reopen the record. The filter works in the other direction too. Video bytes in lesson 24 will show a number that makes an application-tier download path indefensible. The method is identical: compute, then let the number choose the box.
You will show the arithmetic in the packet even when the result is "small." Reviewers trust a small number they can recompute. They do not trust "this will not scale" with no units.
APIs are the failure contract
An API in a high-level design is a short contract:
- Who may call it, and how that caller is identified.
- The identifier scheme.
- The success body or the redirect.
- The response when the resource is absent, revoked, or not allowed.
- Whether the operation is safe to retry.
- What the operation does not promise (freshness, ordering against a second region, an arbitrary destination).
For redirects, the status code is the contract. 302 Found sends the browser to a URI you constructed. 404 Not Found means we have no such code. 410 Gone means we had one and it was revoked, which is a different fact from absence and lets a client stop retrying. 429 and 503 are different again: the former is the caller's rate, the latter is our inability to answer. Collapsing all of these into 200 with an error field pushes the failure design into every client.
Idempotency belongs in the contract when a client will retry a create. A permalink create that allocates a new code on every retry litters the table. Accepting a client token, or treating "same instructor, same lesson, same audience" as the same row, is a design choice. Lesson 37 goes deep on exactly-once claims. Here you only need to say whether a retry creates a second code.
Data ownership
Every fact has a writer. The permalink row is owned by the permalink store. The lesson title is owned by the catalog. A redirect response may copy the title into an HTML preview later; that copy is not allowed to become the source the catalog team edits. This rule is the same one lesson 2 used when a price cache was forbidden from authorizing a charge.
Write, in a table: fact, writer, readers, acceptable staleness, retention. If staleness is "zero," you do not put a cache on the read path, and you accept the latency that follows. If staleness is "up to 60 seconds after revoke," you may cache, and the revocation scenario's response measure must match that window. A scenario that says "revoke is immediate" and a cache that says "TTL one hour" are a failed design, even if both documents look polished.
Keys are part of ownership. A code that is a hash of the lesson id is stable and guessable. A random code with enough entropy is unguessable and must be stored. The choice is a security scenario, not a style preference. State the entropy and the threat: an unauthenticated caller brute-forcing codes to discover unlisted lessons.
Components earn their keep
A component in this phase is a runtime boundary with a responsibility and a failure mode. Typical legitimate reasons to draw a box:
- It owns a store.
- It terminates TLS or authentication at a trust boundary.
- It is a third-party system you do not control.
- Its scale path or its release cycle is genuinely different, and you have the estimate that says so.
"It might be a microservice later" is not a reason in the first sketch. Lesson 7 already recorded why TechLearningPro commerce stayed a modular monolith until a constraint forced a split. Permalink resolution can be a module in the existing classroom application until an estimate or a team boundary says otherwise. Drawing it as its own deployable on day one imports an operational tax: another pipeline, another dashboard, another partial failure. The method asks you to pay that tax only when a scenario requires it.
The scale path is one sentence
After the components exist, write one sentence of the form: "Under ten times today's peak read rate, the permalink primary's read QPS saturates before the application CPU; the next tactic is a cache of code-to-lesson mappings with the revocation window already accepted." That sentence is the scale path. It names the multiplier, the element, and the tactic you have not necessarily built. A diagram with a cache, a shard, a queue, and a second region has four scale paths and no commitment. Leave the unused tactics on a list, the way ADD leaves unused drivers visible.
Failure modes are part of the design, not an appendix
For each external call and each write, answer four questions:
- How do we notice? A timeout, a health signal, a reconciliation job, a learner report.
- What does the caller experience? A redirect, a gone page, a retryable error, a slightly stale lesson.
- What is persisted? A row committed, a row absent, a cache entry that outlives the row.
- What do we refuse to do automatically? Retry a non-idempotent create forever; follow a redirect to a host we do not own; serve an enterprise lesson from a public cache.
Partial failure is the normal case. The database can be up while the identity provider times out. The cache can be full of revoked codes while the database is correct. The application can be healthy while it is answering slowly enough that clients time out and retry, doubling the load. Write the response for the slow case, not only for the down case. A timeout budget on the store call is a design decision. An unbounded wait means the latency scenario has no enforcement.
Security failures belong in the same list. An open redirect — a code that sends the browser to an attacker host — is a failure mode with a concrete fix at design time: do not store arbitrary destinations. If the product later requires arbitrary destinations, the fix becomes an allowlist of schemes and hosts, plus a review of phishing impact. Pretending permalinks and a general shortener are the same product hides that fork.
The decision record closes the loop
Lesson 10's record shape is the close of the method. One decision per record. For permalinks the decision that actually changes the design is whether the row stores a target URL or stores lesson and course identifiers and builds the URI in code. Options, consequences, and a reopen condition ("product requires links to partner sites") make the Thursday whiteboard unnecessary. A second record might cover "module inside classroom, not a new deployable," with a reopen condition tied to QPS or to a team split.
A record that only says "we chose Redis for scalability" has skipped the sequence. There is no scenario, no estimate, no rejected option.
Where the primers stop
/learn/high-level-design/hld-vs-lld and /learn/system-design/what-is-system-design supply the shared vocabulary. A staff review also wants an assumption log, a failure table with persisted state, and an ADR. Lesson 49 is the long URL-shortener case. Lesson 68 is the interview clock. The sequence here is what those later lessons apply.
Architecture and Design Diagrams
The sequence itself is the workflow. A review walks top to bottom. A dashed return means a later step invalidated an earlier assumption, which is a successful outcome, not a restart from a blank board.
mermaidflowchart TDbehavior[Clarify behavior and out of scope]scenarios[Scenarios, constraints, labeled assumptions]estimates[Order-of-magnitude estimates]apis[API contracts including failure statuses]data[Data ownership and staleness]components[Components that own those facts]scale[Scale path: first saturated element]failures[Failure modes and user-visible responses]adr[Decision record with reopen condition]behavior --> scenariosscenarios --> estimatesestimates --> apisapis --> datadata --> componentscomponents --> scalescale --> failuresfailures --> adrestimates -. assumption broke .-> scenariosfailures -. response contradicts scenario .-> scenarios
The runtime picture for lesson permalinks stays small on purpose. The classroom application owns the permalink module. The catalog remains the owner of lesson existence. The identity provider is outside the trust boundary. The cache is optional and disposable.
mermaidflowchart LRlearner[Learner browser]instructor[Instructor]edge[TLS edge]app[Classroom app: permalink module]idp[Identity provider]db[(Permalink table)]catalog[Catalog module]cache[(Code cache, disposable)]learner --> edgeinstructor --> edgeedge --> appapp --> idpapp --> dbapp --> catalogapp --> cache
Resolution is a read path with a strict destination rule: the URI is built from identifiers, then returned as a redirect. Revoke is a write that must reach the table, then drop the cache entry.
mermaidsequenceDiagramparticipant B as Browserparticipant A as Permalink moduleparticipant C as Cacheparticipant D as Permalink tableB->>A: GET /p/{code}A->>C: lookup codealt cache missA->>D: select active rowD-->>A: courseId, lessonId, or noneA->>C: store mapping with TTLendalt active mappingA-->>B: 302 Location built from idselse revoked or missingA-->>B: 410 or 404end
Technical Explanation
Read path, write path, and the revoke window
Creation is a write from an authenticated instructor or a staff tool. The module checks that the caller is allowed to share that lesson, allocates a code, and inserts a row: code, course id, lesson id, audience, creator, timestamps, and a null revoked-at. The code is the primary key. There is no target-URL column.
Resolution is a read. The module loads the row, or a cache entry that was filled from a row. If revoked_at is null and the lesson is still shareable, it builds an absolute https URI on the TechLearningPro host with the course id and lesson id as path segments, and responds 302. If the row is missing, 404. If revoked_at is set, 410. The browser then requests the lesson page, which performs its own authorization. A permalink is not a capability token for a private enterprise lesson. That split is deliberate: the redirect proves we recognize the code; the lesson page proves the caller may see the content.
The cache stores the mapping from code to ids plus the revoked bit. It does not store HTML and it does not store a learner's session. On revoke, the module updates the row and deletes the cache key. A failure to delete the key leaves a stale "active" entry until the TTL. The revocation scenario therefore cannot honestly say "immediate" unless the TTL is zero, which means there is no cache. The record picks a 60-second TTL and sets the response measure to match: after revoke, at least 99 percent of resolutions observe 410 within 60 seconds, measured at the API.
Authorization placement
Two checks exist, and they protect different facts.
The create check asks whether this caller may mint a link for this lesson. It runs in the permalink module and reads catalog policy. An instructor of the course can mint an unlisted link. A learner cannot. A support tool can, under an audited role.
The lesson check asks whether this caller may view the content. It runs in the catalog and enrollment modules after the redirect, using the session the learner already has. An unlisted permalink shared in a public chat can still land on a login page. That is the correct outcome for a private course. Treating the code as a bearer token that bypasses enrollment would make every leaked chat log a data breach. The security scenario names that stimulus explicitly so a later "growth hack" cannot collapse the two checks.
Code space
A code is an unguessable identifier, not a hash of the lesson. Hashing the lesson id makes one stable code per lesson, which is convenient and also lets anyone compute it. For unlisted sharing, the threat is enumeration. Seven characters from a 62-symbol alphabet is 62^7, about 3.5 trillion values. At a billion guesses a day, which is far above a credible abusive client that has passed the edge rate limit, the expected time to hit one specific code is still measured in millennia. The birthday problem matters when we have minted a large set and worry about collisions on insert, not when an attacker hunts one code. With millions of rows, collision probability on a uniform 3.5-trillion space is negligible; the insert still handles a unique-key conflict by drawing again, because negligible is not a proof of a single insert.
The rate limit at the edge is what makes the entropy argument true in practice. Without it, the attacker's budget is "as fast as the network allows." Rate limiting as a design is lesson 35. The permalink packet says "enumeration resistance assumes an edge limit; the limit's algorithm is not designed in this iteration," and leaves that driver visible.
What we deliberately do not decide yet
Indexing strategy beyond the primary key, the cache product, multi-region replication, and a public marketing shortener that can target partner hosts are out of this iteration. Naming them in the out-of-scope list is how the next review starts faster. Lesson 27 will be allowed to choose a cache technology when the read scenario demands it. Lesson 49 will design arbitrary-URL shortening, where the open-redirect problem returns in a harder form.
Consistency and the lesson title
The redirect does not include the lesson title. Titles change, and the catalog owns them. Coupling the permalink row to a copied title creates a second writer problem whenever an instructor renames a lesson. The cost is an extra round trip after the redirect, which the lesson page was going to pay anyway. This is a small example of the general rule: do not denormalize a fact into another owner's store unless a scenario's measure requires it and a refresh path exists.
Practical Implementation or Design Walkthrough
Problem statement
Cohort instructors paste lesson URLs that wrap, expire in chat clients, and expose internal query parameters. TechLearningPro will issue short permalinks on its own host. The classroom application already exists. The identity provider already exists. We will not build a general internet shortener in this iteration.
Behavior in scope
- An authorized instructor creates a permalink for a lesson in a course they teach.
- Anyone with the code receives a redirect to the canonical lesson URL, or a not-found, or a gone.
- The instructor, and staff with the role, can revoke.
- The lesson page enforces enrollment and tenant policy after the redirect.
- Creates are safe to retry with the same idempotency key.
Out of scope
- Arbitrary destination URLs.
- Per-click analytics pipelines.
- Custom vanity words as the only code (a later option can reserve a word and still store a random code).
- A new deployable service.
- Multi-region active-active writes.
Scenarios
P-read. Source: an anonymous or signed-in browser. Stimulus: GET /p/{code} for an active code. Artifact: the permalink module. Environment: steady evening load, table reachable, cache warm or cold. Response: 302 to the canonical lesson URI on our host. Measure: p95 under 80 ms and p99 under 200 ms at the application, over 15 minutes, excluding the browser's subsequent lesson fetch.
P-revoke. Source: the instructor. Stimulus: revoke. Artifact: the permalink module and its cache. Environment: resolutions in flight. Response: the row records revoked_at, and later resolutions return 410. Measure: 99 percent of resolutions after a successful revoke response observe 410 within 60 seconds.
P-authz. Source: a person who is not enrolled. Stimulus: follows a permalink for a private cohort lesson. Artifact: the lesson page, after redirect. Environment: valid code, no enrollment. Response: authentication or a forbidden page, and no lesson body. Measure: the lesson payload is absent from the response; the permalink response itself contains only a location on our host.
Assumptions
| Input | Value | Label |
|---|---|---|
| Active permalinks after one year | 20 million | assumption, product guess |
| Resolutions per day | 5 million | assumption |
| Peak-to-average ratio | 5 | assumption, evening cohort overlap |
| Create and revoke per day | 50,000 writes | assumption |
| Row size including indexes | 400 bytes | assumption |
| Revocation staleness we will accept | 60 seconds | product decision, not a guess |
Average read rate is 5e6 / 86400 ≈ 58 QPS. Peak is about 290 QPS. Writes are under 1 QPS average. Stored bytes are 20e6 × 400 ≈ 8 GB, before a replica. A single primary plus one replica is inside the estimate with room. The scale sentence can still name a cache, because p95 under 80 ms on a cold primary during a noisy neighbor is the risk, not raw QPS. The cache is a latency tactic, and the 60-second window is its tax. Sharding is not indicated. Lesson 24 would push on these inputs if this were a capacity review; the method only needs them to be visible.
API
POST /api/permalinks with a bearer token, a body of courseId, lessonId, and audience, and a required Idempotency-Key header. 201 returns the code and the public URL. A replay with the same key and the same body returns the same 201 body. The same key with a different body returns 409. 403 when the caller may not share that lesson. 404 when the lesson does not exist.
GET /p/{code} is unauthenticated. 302, 404, or 410 as above. No response body on the redirect.
POST /api/permalinks/{code}/revocations with a bearer token. 204 when the row is now revoked, including when it was already revoked. 404 when the code does not exist. 403 when the caller is not allowed to revoke.
Data
| Fact | Writer | Readers | Staleness | Retention |
|---|---|---|---|---|
| Code, course id, lesson id, audience, creator | Permalink module | Resolver, instructor UI | Cache may lag revoke by the TTL | Life of the course, then 1 year |
| Lesson exists, title, tenant visibility | Catalog | Lesson page, create-time check | Catalog's own rules | Catalog policy |
| Enrollment | Enrollment module | Lesson page | Enrollment's own rules | Enrollment policy |
The redirect URI is not a stored fact. It is a function of the ids and the configured host.
Component and failure sketch
The permalink module ships inside the classroom Spring Boot application, behind the existing TLS edge. It calls the catalog module in process for the create-time check. It reads and writes its own table. A cache client is allowed only for resolution. If the cache client errors, the module reads the table and still answers; a cache outage is not a permalink outage. If the table times out, the module returns 503 and does not guess. If the identity provider times out on create or revoke, those writes fail with 503 and do not insert a row. Resolution does not call the identity provider.
That last sentence is a failure-mode decision. Coupling anonymous redirects to the identity provider would make every identity incident an outage of shared links, including links opened by people who are already signed in at the lesson page. The lesson page still depends on identity. The redirect does not.
Decision record
ADR-101: Permalink rows store lesson identifiers, not destination URLs
- Status: Accepted
- Context: Instructors need short links to lessons. Marketing has asked, informally, for links to partner blogs. The security scenario P-authz and the open-redirect failure mode are in play. The classroom application and the catalog already exist.
- Drivers: P-authz; operational simplicity; the estimate that shows a few hundred peak QPS.
- Options: (A) Store an arbitrary URL and redirect to it. (B) Store course id and lesson id and build an
httpsURI on our host. (C) Store an arbitrary URL but allowlist hosts. - Decision: Option B for this iteration. The public host is configuration. Path segments are escaped with Spring's
UriComponentsBuilder. Scheme must behttps. - Consequences: Partner links are impossible until we reopen. Revoke and authorization stay well defined. A bug in URI building can still produce a bad path; it cannot send the browser to another registrable domain unless the configured host is wrong. Option C is the documented alternative if marketing's request becomes a constraint.
- Reopen when: a product owner brings a partner-link scenario with a named host allowlist and a phishing review. A reopen is a new record, not an edit that hides B.
ADR-102: Permalink resolution stays a module in the classroom application
- Status: Accepted
- Context: The estimate is hundreds of QPS, not hundreds of thousands. The team operating classroom is the team that would be on call for a new service.
- Options: a new deployable, or a module with its own table.
- Decision: a module. The table is not shared with catalog aggregates.
- Consequences: a bad deploy of classroom can take permalinks with it. We accept that because we do not have a separate team, and because splitting now would create a partial failure we would then have to design. Reopen when peak sustained read QPS exceeds 5,000 or a separate squad owns sharing.
Implementation sketch
The controller is the contract, not the design. The design is the decision that lessonUri() is built from ids we stored. PermalinkService.findActive returns an empty optional for unknown codes and a revoked result for 410. Those branches are the failure contract.
javapackage com.techlearningpro.classroom.permalink;import java.net.URI;import java.util.Optional;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.util.UriComponentsBuilder;@RestControllerpublic class PermalinkController {private final PermalinkService permalinks;private final PermalinkProperties properties;public PermalinkController(PermalinkService permalinks, PermalinkProperties properties) {this.permalinks = permalinks;this.properties = properties;}@GetMapping("/p/{code}")public ResponseEntity<Void> resolve(@PathVariable String code) {return switch (permalinks.lookup(code)) {case PermalinkHit.Active active -> ResponseEntity.status(HttpStatus.FOUND).location(lessonUri(active.courseId(), active.lessonId())).build();case PermalinkHit.Revoked ignored -> ResponseEntity.status(HttpStatus.GONE).build();case PermalinkHit.Missing ignored -> ResponseEntity.notFound().build();};}private URI lessonUri(String courseId, String lessonId) {URI uri = UriComponentsBuilder.fromUriString(properties.publicHost()).path("/courses/{courseId}/lessons/{lessonId}").buildAndExpand(courseId, lessonId).encode().toUri();if (!"https".equalsIgnoreCase(uri.getScheme()) || uri.getHost() == null) {throw new IllegalStateException("permalink host must be https");}return uri;}}
PermalinkHit is a sealed interface with three records: Active, Revoked, and Missing. The compiler then forces the controller to handle revoke differently from absence. PermalinkProperties.publicHost is bound with @ConfigurationProperties, the same mechanism Spring Boot uses for typed configuration. The host check is defensive. The architectural control is still ADR-101: there is no column a caller can fill with https://evil.example.
Create uses a unique constraint on the idempotency key. A retry that finds the key returns the original code. This sketch does not show the transaction; the contract does. If the insert commits and the HTTP response is lost, the client retries and receives the same code.
Validation
- A test inserts a row and expects
302with a location whose host equals the configured host and whose path contains the ids. - A test revokes and expects
410, including a second revoke. - A test uses an unknown code and expects
404. - A test follows the location with a principal who lacks enrollment and expects no lesson body. That test belongs to the lesson page, and the permalink review does not pass without it.
- A test sets the configured host to an
httpURI and expects the resolver to fail closed. - A load check at roughly five times the assumed peak confirms p95 still sits inside P-read, or it produces a new assumption. One laptop run is not the measure.
Expected result
A reviewer can recompute the QPS, point at the writer of each fact, name the user-visible result of a table timeout, and find the reopen condition for partner links. They cannot find a bus, a shard map, or a stored arbitrary URL. That absence is the work.
Enterprise Real-World Scenarios
The whiteboard that arrived before the question
TechLearningPro's growth review opened with a container diagram: an API gateway, a permalink service, Redis, Kafka "for click events," and a read replica in a second region. The estimate had not been written. When the facilitator forced step 1, two product statements collided. Instructors wanted unlisted links to lessons inside a private cohort. Marketing wanted short links to conference blogs on other domains. Those are different threat models. The first is an identifier problem with a redirect built in code. The second is an open-redirect problem with a phishing impact on every learner who trusts techlearningpro.example.
The review threw away the diagram and wrote ADR-101 in the room. Kafka for click events moved to the unaddressed list: no scenario had a measure that required a stream, and the on-call team was the classroom team. The second region moved to the unaddressed list with a note: "reopen if a residency constraint appears; do not reopen because the stencil had two clouds." Three months later marketing returned with a single partner host. The new record accepted option C for a separate marketing-link module, with an allowlist of one host, a distinct path prefix, and no reuse of lesson codes. The original permalink table stayed identifier-only. The sequence made the second decision cheap because the first decision had a reopen condition instead of a vibe.
The failure mode that justified the split was specific. A researcher registered a look-alike path on a partner site once option C existed, and the allowlist checked the registrable host, not a prefix that an attacker could extend. The post-incident change was a stricter host comparison and a display interstitial for off-site marketing links. Lesson permalinks never showed the interstitial, because they cannot leave the host. Mixing the two products in one table would have forced every lesson redirect through a warning, or every marketing link through no warning.
Revoke during a live cohort
An instructor shared a permalink to a draft lesson, taught the session, and revoked it at the break after realizing the draft included a staff-only note in the page body. The note was a catalog bug of its own. The permalink question was whether revoke meant anything while twenty browsers still had the code.
The first implementation cached mappings for 24 hours and deleted nothing on revoke. Resolutions kept returning 302. The lesson page, once fixed, hid the note, so the impact was limited, but the permalink contract was false: support told the instructor the link was dead, and learners' browsers disagreed for a day. The repair was the P-revoke measure: update the row, delete the key, TTL 60 seconds, dashboard on the age of 410 after revoke. The instructor-visible copy changed too. The revoke button says links stop working within a minute, which is the truth the architecture can keep.
A second defect showed up in the same incident. Create had been calling the identity provider on every resolution "to be safe." When the provider slowed down that evening, permalinks timed out even for public lessons, and the classroom home page, which did not need permalinks, stayed up. The failure table was amended: resolution does not depend on the identity provider. Creates and revokes do. The dependency was drawn on the sequence diagram so the next person cannot "safen" the read path by adding a call.
Best Practices
- Write the out-of-scope list in the same sitting as the behavior list. A design that can do anything has not started.
- Keep assumptions in one table and point every derived QPS or byte figure at a row. If you change an input, recompute in the open.
- Use the six-part scenario form for the two or three qualities that can kill the design. Leave the rest on the unaddressed list.
- Document
404,410,403,409, and503as carefully as200or302. The happy path is the easy part of the contract. - Give each stored fact one writer. If a cache holds the fact, write the maximum age next to the scenario that cares.
- Draw the smallest component set whose failure modes you are willing to operate. Add a box only when it owns data, a trust boundary, or a proven scale limit.
- Write the scale path as one sentence with a multiplier and a saturated element.
- For every dependency, state the timeout and the response when the timeout fires.
- Close with an ADR that names the rejected option and a reopen condition a future product request can match.
- In an interview, narrate the sequence out loud and label guesses. Interviewers can forgive a wrong guess. They cannot review a silent one.
- When a mechanism is suggested early, park it in a column called "not yet earned" and return to it at step 7.
Common Mistakes and Anti-Patterns
The mechanism catalog. The diagram contains every system the author has read about, and the behavior still fits in a sentence. It happens because drawing boxes feels like progress and asking scope questions feels slow. The consequence is operational surface area with no owner. The repair is the sequence: no box until step 6, and step 6 has to point at a fact or a trust boundary.
Happy-path APIs. The write-up shows POST and 200 and stops. Revoke, conflict, timeout, and forbidden are discovered by the first client. The repair is a status table before the component diagram. If you cannot fill a cell, the design is unfinished.
Stored destinations by default. Short links are modeled as "string in, string out" because that is the generic article. For a product that only shares its own lessons, the stored string is an open redirect waiting for a bug. The repair is ADR-101's question: what is the destination a function of? Arbitrary URLs are a different product with an allowlist, a distinct path, and a phishing scenario.
Cache as a default tier. A cache is drawn before anyone accepts staleness. Revoke "feels" instant and is not. The repair is to write the acceptable window into the scenario first. If the window is zero, omit the cache and record the latency you accepted instead.
Estimates with no units. "Millions of users" is used to justify sharding. Registered users are not QPS, and QPS is not concurrent video bitrate. The repair is lesson 24's discipline, even when you only need one table: events per day, divided by 86,400, times a peak factor, with the peak factor labeled.
Failure modes as a slogan. "We will retry" or "we are highly available" ends the section. Retries without idempotency duplicate permalinks. Availability without a definition of down hides whether a cache miss storm counts. The repair is the four-question failure note: notice, caller experience, persisted state, what we will not do automatically.
New deployable as a badge. The permalink module becomes a service because the stencil says microservices. The estimate is 300 QPS and the on-call rotation is one squad. The consequence is a network failure mode between two processes the same team ships. The repair is ADR-102: stay in-process until a number or a team boundary reopens the record.
Troubleshooting and Design Review
Symptom: The review spends its time naming technologies, and nobody can state what revoke does.
Likely cause: the sequence started at components. Behavior and scenarios were skipped or left as adjectives.
Investigation: ask for the out-of-scope list, the three scenarios, and the status codes. If they are missing, stop reviewing boxes.
Corrective action: reconstruct steps 1 through 4 on one page. Delete any box that does not own a fact those steps named.
Symptom: Two weeks after launch, a revoked link still redirects, and support has no number to quote.
Likely cause: a cache TTL longer than the promise, or a revoke path that updates the row and leaves the key.
Investigation: compare P-revoke's measure to the TTL and to the code path that deletes the key. Check whether a failed delete is logged.
Corrective action: align the promise, the TTL, and the delete. If the product now requires a tighter window, drop the TTL and accept the latency, or delete synchronously and alarm when delete fails.
Symptom: Permalink latency tracks identity-provider latency.
Likely cause: resolution calls the provider even though the lesson page will authorize after the redirect.
Investigation: a trace of GET /p/{code}. The dependency list in the failure table should have forbidden this.
Corrective action: remove the call from the read path. Keep it on create and revoke. Add a test that resolves a public code with the provider stubbed to sleep.
Symptom: Adding application instances does not move p95, and the primary's CPU is pegged.
Likely cause: the scale sentence was wrong, or it was never written and the cache was deferred past the point the estimate allowed.
Investigation: read QPS against the assumption table. Confirm the cache is enabled and that the hit ratio matches a 60-second TTL under this key distribution. A few viral codes should be hot; a uniform miss pattern means the TTL or the key is wrong.
Corrective action: fix the cache before sharding. If the miss path is the saturated element and the hit ratio is already high, the next record can introduce a read replica. Sharding stays unearned at this size.
Symptom: A permalink returns 302 to a host that is not ours.
Likely cause: ADR-101 was violated by a column, a configuration value, or a later marketing feature folded into the same resolver.
Investigation: read the location header in a test. Inspect the row shape. Search for any persisted URI.
Corrective action: fail the deploy. Split marketing links into their own module and allowlist, as the reopen condition described. Do not "temporarily" widen the host check.
Design-review questions for this lesson: What is out of scope? Which three scenarios can fail the design, and do their measures contain a number and a window? Which inputs are assumptions? What are the status codes for absence, revoke, conflict, and dependency failure? Who writes each fact, and how stale may a read be? Which element saturates first under the stated multiplier? For the database, the cache, and the identity provider, what does the user see when each is slow? Which ADR records the option we refused, and what reopens it?
Interview Questions and Answers
Q1. What is the order of a high-level design, and which step is most often skipped?
Behavior and an out-of-scope list, then scenarios with constraints and labeled assumptions, then estimates, APIs, data ownership, components, the scale path, failure modes, and a decision record. The step most often skipped is failure modes, followed closely by labeled assumptions. Diagrams appear early because they are visible. I keep components at step 6 so every box has a fact or a trust boundary already named. If the interviewer pushes for a picture sooner, I sketch a placeholder and say it is not yet a design.
Q2. What is the difference between a constraint and an assumption in this method?
A constraint is a condition the design does not get to remove: an identity provider we already operate, a rule that enterprise lesson bodies stay in-region, a team of one squad. An assumption is a number or a fact we are using until it is measured, such as five million resolutions a day or a peak-to-average ratio of five. Both belong in the packet. Treating an assumption as a constraint freezes a guess. Treating a constraint as an assumption invites a diagram that violates residency or compliance.
Q3. How do you know a high-level design is detailed enough to stop?
I can recompute the peak rate from the assumption table, name the writer of each fact, list the status codes for the unhappy paths, state the timeout and the user-visible result for each dependency, and point to a record that contains the rejected option and a reopen condition. I do not need a shard key, a vendor logo, or a class diagram. If a quality scenario is still an adjective, I am not done. If I am designing a second region that no scenario mentions, I have gone too far.
Q4. Why does the permalink design refuse to store destination URLs?
Because the product behavior is "open this lesson," and a stored URL is a different behavior: "send the browser wherever this string says." The second behavior is an open redirect and a phishing channel on a host learners trust. Storing course and lesson ids and building an https URI on a configured host makes the destination a function of data we own. I would reopen the record for a separate marketing product with an allowlist, a different path, and its own scenario. I would not widen the lesson table.
Q5. How do you decide whether a cache belongs in the first sketch?
I look at the read scenario's measure and at the freshest write that must become visible. For permalinks, p95 under 80 ms at a few hundred QPS may be achievable from the table alone; the cache is a latency tactic, not a survival tactic. It is allowed only after product accepts a revoke window at least as long as the TTL. If they require instant revoke, I leave the cache off and put the primary's latency in the scenario. A cache drawn "because we always have Redis" has no owner and no window.
Q6. What do you put in a failure-mode note for the database on the read path?
How we notice: a timeout on the query, shorter than the API budget. What the caller sees: 503, not a guessed redirect. What is persisted: nothing changes on a read timeout. What we will not do: fall back to a stale cache entry if we cannot tell whether it was revoked, unless the scenario explicitly allows that degraded mode. I also state that a cache client error is different: we skip the cache and read the table. One dependency being down is not "the system is down."
Q7. How should create behave when the client retries?
The create contract requires an idempotency key. The first committed insert wins, and a retry with the same key and the same body returns the same code. The same key with a different body is 409. Without that contract, a timeout after a successful insert mints a second permalink and the instructor shares the wrong one. I do not need a distributed transaction for this. I need a unique key and a lookup. Lesson 37 is where I would go if the interviewer wants the general theory.
Q8. When would you split permalinks into a separate deployable?
When a number or an organizational boundary makes the module's failure domain too large. The record I would write says: reopen above a sustained peak we name, or when a different squad owns sharing and needs its own release train. "Microservices scale better" is not a split reason. At a few hundred QPS the database and the revoke window dominate the design, and a process boundary adds a failure mode without moving those limits.
Q9. A partner asks for links to their site. How do you change the design without breaking ADR-101?
I open a new record. I do not add a URL column to the lesson permalink table. The new module has its own prefix, stores only URLs whose host is on an allowlist, compares the host precisely, and can show an interstitial. Lesson codes keep resolving through the identifier path. The phishing scenario is reviewed for the new path only. If the allowlist cannot be operated, the answer is to refuse the feature, and the original record stays accepted.
Q10. How do you handle an interviewer who wants the full URL-shortener design in forty minutes?
I run the same sequence on a thinner slice and say what I am not designing. I spend real minutes on the destination rule, the read/write ratio, the status codes, and one failure mode, because those change the picture. I state an assumption for daily creates and resolutions and divide by 86,400 in front of them. I park analytics, sharding, and multi-region on a list with the condition that would pull each one back. Depth on one decision beats a tour of every tier. The full case is a longer exercise; the method is what fits the clock.
Q11. What is a scale path, in one sentence, for this permalink design?
Under ten times the assumed peak of about 290 QPS, the primary's read latency saturates before application CPU; the tactic is a cache of code-to-id mappings whose TTL is inside the 60-second revoke window. If the hit ratio is already high and the primary is still hot, the next tactic is a read replica, not a shard. I can defend that sentence from the assumption table. I cannot defend a sentence that only says "we will scale horizontally."
Q12. How does this method use an ADR differently from a meeting note?
A meeting note records who attended and what was drawn. An ADR records the drivers, the options that lost, the decision, the consequences including the tax, and the condition that makes the decision wrong later. ADR-101 loses arbitrary URLs on purpose and tells marketing exactly how to come back. A note that says "agreed to keep it simple" cannot be tested against a pull request that adds a URL column. The record can.
Hands-On Exercises
Exercise: High-level design for a URL-shortening service
Problem statement. TechLearningPro's marketing site needs a shortener for campaign links. Destinations may include our host and a small set of partner hosts. This is the product ADR-101 refused to mix into lesson permalinks. Produce a high-level design packet. Use your own numbers and label every assumption. Lesson 49 is the long case study; this exercise is the method, kept short enough to review.
Tasks.
- Write behavior in scope and an out-of-scope list. Include whether anonymous users may create links.
- Write three scenarios: redirect latency, abuse or phishing, and one write scenario for create or revoke. Use all six slots and a numeric measure.
- Build an assumption table and compute peak read QPS, peak write QPS, and one-year storage. Show the division by 86,400.
- Specify the API, including status codes for unknown codes, disabled links, forbidden creates, idempotency conflicts, and dependency failure.
- State whether you store a destination URL, and the allowlist rule. Name the writer of each fact and the acceptable staleness.
- Draw the component sketch and one sequence for a redirect. Justify any box that is not the application, the table, or the identity provider.
- Write the scale-path sentence and a failure note for the table, the cache if you have one, and the case of a destination host that later becomes malicious.
- Write one ADR with a rejected option and a reopen condition. Say how this design stays clear of the lesson-permalink table.
Expected deliverables. The packet: scenarios, assumption table with arithmetic, API and data tables, one diagram, failure notes, and the ADR.
Evaluation rubric.
| Assessment area | Weight | What "good" looks like |
|---|---|---|
| Requirements and assumptions | 15% | Scope and out-of-scope are explicit; every number is labeled and recomputable |
| Architecture and design quality | 25% | Components match data ownership; no box without a job |
| Technical correctness | 20% | Status codes, idempotency, and the redirect rule are coherent |
| Scalability, reliability, and security | 20% | Scale sentence names an element; open redirect and dependency timeouts are specified |
| Trade-off reasoning | 10% | The ADR records a real tax and a reopen condition |
| Documentation and communication | 10% | A reviewer can follow the sequence without asking what a box means |
An unlabeled "millions of users," a stored URL with no host rule, or a cache with no staleness window means the packet is not ready for a human review.
Lesson Summary
High-level design is a sequence with an artifact at the end. Behavior and out-of-scope come first, then scenarios, constraints, and labeled assumptions, then estimates that are allowed to be rough if they are shown, then APIs that include failure statuses, then data ownership and staleness, then the smallest component set, then one scale-path sentence, then failure modes with persisted state, then a decision record that names the option refused.
TechLearningPro lesson permalinks are the worked slice. Identifiers in the row, URIs built on our host, revoke as 410 within a window the cache is allowed to use, resolution independent of the identity provider, and a module inside the classroom application until a number or a team boundary says otherwise. The primers introduce the words. The case studies later in this course reuse this sequence. A diagram that cannot survive the failure step is still a whiteboard.
Lesson 24 takes the estimate step and does it properly: daily actives, QPS, storage, bandwidth, retention, and the way a wrong peak factor chooses the wrong architecture.
Further Reading
- Bass, Clements, and Kazman, Software Architecture in Practice — scenarios and the discipline of writing a response measure before selecting a structure.
- Cervantes and Kazman, Designing Software Architectures — iterative design and the habit of leaving drivers unaddressed on purpose.
- Newman, Building Microservices, the chapter on when not to split a deployable — the operational tax behind ADR-102.
- RFC 9110 — HTTP semantics for
302,404,409,410, and503, which this lesson uses as a contract rather than as decoration. - C4 model — context and container views, once the sequence has earned a picture. Lesson 10 shows how those views live next to ADRs.
- AWS Well-Architected Framework — a question bank. Answer the reliability and security questions from the failure table, not from a service list.
- Primers: /learn/high-level-design/home, /learn/high-level-design/requirements, /learn/system-design/requirements-clarification, /learn/system-design/interview-framework.
- This course: /learn/advanced-system-design/architectural-thinking-and-quality-attributes, /learn/advanced-system-design/architecture-documentation-and-adrs, /learn/advanced-system-design/capacity-estimation-and-workload-modeling, /learn/advanced-system-design/design-a-url-shortening-service.