01Two planes, one-way propagation
The management plane is the only writer; the redirect plane reads a projection and a deny-set and holds no write credential. A four-hour management outage costs zero redirects.
ADR-0123 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.
The service behind every short link anyone has ever tapped without thinking: a code goes in, a redirect comes out, 150,000 times a second, and the destination can still be changed after the code is printed on ten thousand cards. Read the set in order — the boundary first, then the people, then the structure that serves them. One decision runs through all twenty-three views: the redirect plane and the management plane are separate systems joined only by one-way propagation, and the redirect plane never writes.
The redirect plane and the management plane are separate systems joined only by one-way propagation, and the redirect plane never writes. Everything the platform wants from a click happens after the redirect has already been sent.
A short link is the most disposable-looking artefact in software and one of the least changeable. It goes into an email, onto a poster, into a QR code on a badge, and from that moment the platform cannot reach it. What it can change is where the code points — and that is the product. So the system has to do three things that pull against each other: answer 150,000 lookups a second from everywhere on earth in single-digit milliseconds; let an owner repoint or kill a code that is already printed on ten thousand cards, and have that take effect globally in under a minute; and count every click accurately enough to bill on, without ever putting a write in front of a read that outnumbers writes 500 to 1. The obvious architecture — one service, one database, look up the code, increment the counter, redirect — satisfies all three on a whiteboard and none of them in production.
Codes are claimed once, by a conditional create against a strongly consistent reservation index, and are then immutable for the life of the platform: never reassigned, never reissued, never reclaimed. The link record is written only by the management plane, in one write region, and is the single authority for what a code means. Resolution reads a projection of it, never the record itself on the hot path: Front Door terminates TLS at the nearest of 190+ PoPs for both the shared and the 12,000 branded domains, caches the redirect for 30 seconds, and forwards a miss to the nearest of twelve stateless read regions, where a resolver checks the deny-set first, coalesces concurrent misses, then walks an in-process LRU, a regional Redis hot set and an in-region Cosmos replica. Two propagation channels cross from management into resolution, with two published bounds: a compact revocation delta that reaches every region within 60 seconds, and a full projection off the change feed within five minutes. Clicks are counted from the edge access log rather than from application code, which is what lets a redirect be cached without losing the click, and which keeps every write off the read path. Destinations are screened before a code is issued and re-screened afterwards, because a destination that was clean in March can be phishing in June.
The management plane is the only writer; the redirect plane reads a projection and a deny-set and holds no write credential. A four-hour management outage costs zero redirects.
ADR-01Stop instructions travel as a compact delta with a 60-second bound, independent of the five-minute projection. Urgency is bought where it is needed and nowhere else.
ADR-02Seven unguessable base62 characters, claimed by conditional create, never reassigned and never reissued after retirement. (ADR-04 carries the rest.)
ADR-03Alias uniqueness. Everything else on the read path is eventually consistent, with its staleness published as a number rather than implied as instant. (ADR-15 carries the rest.)
ADR-05Slower and more expensive than a permanent redirect, and the only choice that keeps a printed link both countable and killable.
ADR-08Click events come from the edge access log after the response has left, durably buffered, with a 0.05% loss budget and daily reconciliation. (ADR-11 carries the rest.)
ADR-10An assumed 18% of traffic is unfurlers and scanners. They are served, tagged, excluded from customer counts and from tenant quotas, and kept in the raw data.
ADR-12Destinations are checked before a code is issued and re-checked afterwards; a blocked link gets a platform interstitial that names the reason, never a silent 404. (ADR-14 carries the rest.)
ADR-13Twelve thousand customer domains mean certificate renewal is an availability risk the platform owns and alarms on before expiry, not a customer responsibility.
ADR-16A link platform outlives everything around it, because its output is printed, embedded and forwarded by people who will never contact it again. These are the properties that should survive a change of scale, of cloud, and of the team.
Every other decision here can be revisited. This one cannot, because the evidence of its violation arrives years later as a QR code on a poster resolving to a stranger's site. It depends on no technology and costs almost nothing to keep — which is exactly why a future team, looking at a table of desirable retired aliases, will be tempted to break it.
ADR-01 says the redirect path reads a projection and never writes. That holds if Front Door is replaced by a programmable edge, if Cosmos DB is replaced by anything with a point read, and if the whole thing moves to another provider. What would break it is one feature request — real-time click counts on the redirect response — which is why the boundary is drawn in views 08 and 14 rather than left as a convention.
The 60-second revocation bound and the five-minute edit bound will both be argued about, and both may tighten as technology improves. What must not change is that the platform states a number instead of implying immediacy, because support answers, customer expectations and abuse response all derive from that number being sayable in one sentence.
It is the reason caching and analytics are not in conflict, and it will still be the reason on any edge platform that produces a request log. The fragile version of this decision is counting in application code, which looks identical on a dashboard and quietly couples the cache hit rate to the accuracy of the numbers customers are billed on.
ADR-07 puts the lookup in twelve regional resolvers because Azure offers no general-purpose replicated key-value store at the PoP. That is a statement about 2026, not about architecture. When a programmable edge with a replicated store is available and affordable, the lookup should move — and nothing above needs to change when it does, which is the test of whether the boundary was drawn in the right place.
Every number here is a stated assumption from the requirement, chosen to be argued with. The right-hand column names the view where the mechanism that meets it is drawn.
| Quality | Target | How it is met | View |
|---|---|---|---|
| Redirect availability | ≥ 99.99% monthly per region | Anycast edge with a 30 s response cache, twelve stateless read regions, and no synchronous dependency on the management plane or the click pipeline. | 18 |
| Redirect latency | p50 ≤ 8 ms, p99 ≤ 40 ms; ≤ 120 ms p99 on a cold read | Edge cache, then an in-process LRU, a regional Redis hot set and an in-region Cosmos replica, with concurrent misses coalesced per code. | 14 |
| Correctness | Zero codes resolving to a destination they were not assigned | Conditional create against a strongly consistent reservation index, permanent code retirement, and a continuous synthetic wrong-destination probe. | 15 |
| Revocation | Effective globally ≤ 30 s p95, ≤ 60 s p99 | A compact revocation delta on its own channel, pulled every 5 s, matched before any cache layer; the 30 s edge TTL bounds the worst case. | 16 |
| Edit propagation | ≤ 5 min p99 globally; ≤ 1 s p99 in the creating region | Cosmos change feed to a projection worker in each read region, pre-warming the hot set at creation. | 13 |
| Throughput | 150,000 redirects/s steady, 600,000/s peak for 5 min | 97% served at the edge; resolver fleets scaled on requests per second with 4× headroom for the assumed burst. | 18 |
| Hot key | A single code at 80,000/s without degrading others | In-process LRU ahead of the shared cache, request coalescing, and a hot-set distribution that does not concentrate on one cache shard. | 09 |
| Creation | p99 ≤ 250 ms; 10,000-link import ≤ 60 s p99 | Single-region conditional create plus a durable write, with bulk import as an asynchronous job on separate capacity. | 15 |
| Analytics freshness | Click visible ≤ 60 s p95, ≤ 5 min p99 | Edge access log to Event Hubs to Azure Data Explorer, with rollups as materialised views. | 17 |
| Analytics accuracy | Loss ≤ 0.05%; counts within ±0.5%; uniques ±2%, labelled | Durable buffering at the point of collection and daily reconciliation against the edge counters. | 17 |
| Durability | Link table RPO 0, read RTO ≤ 60 s, write RTO ≤ 5 min | Cosmos DB multi-region replication with a single write region and a named failover target. | 18 |
| Rebuildability | All derived state rebuilt ≤ 30 min, no operator input | Hot set, negative cache and deny-set are all projections of the link table, rebuilt from the change feed; drilled monthly. | 19 |
| Cost | ≤ $0.05 per million redirects; alarm above $0.08 | Edge hit rate ≥ 97% as the primary lever, raw click retention capped at 90 days, bot traffic metered separately. | 20 |
Four of this architecture's claims are cheap to test and expensive to be wrong about. A prototype that proves these can be built on; one that skips them is a demonstration of HTTP redirection, which is the easy half.
| Risk | If it lands | Response |
|---|---|---|
| The edge cache hit rate does not reach 97% | Cost per million redirects rises towards the origin-read price, the resolver fleet must grow several times over, and the 40 ms p99 becomes a cold-path p99 instead. | Measure the real code popularity distribution early. The designed levers are a longer TTL (which weakens the revocation bound and must be traded explicitly), a larger hot set, and pre-warming on creation. |
| Revocation is promised at 60 seconds and delivered at five minutes | The platform's safety claim fails silently: dashboards stay green because the mechanism is healthy while the end-to-end result misses the bound. | The synthetic revocation probe in view 20 is the control, alarmed on the promise rather than on component health. The 30 s edge TTL is a design parameter and any proposal to raise it must state the new bound. |
| A single write region constrains creation availability | Regional failover makes link creation unavailable for up to five minutes, which a large customer's bulk import will eventually hit. | Accepted deliberately: refusing creation is preferable to two winners for one alias. Bulk imports are jobs with resumable semantics, so a failover delays rather than fails them. |
| Bot classification is wrong in the direction customers notice | Either campaigns are judged on preview traffic, or genuine clicks are discarded as bots — and the second is far worse because it is invisible. | Classification tags and never blocks; the raw class is retained so a reclassification can be replayed over history rather than requiring the data to be re-collected. |
| The platform becomes phishing infrastructure | The shared domain's reputation is destroyed by browser and mail-provider blocklists, which takes every legitimate tenant's links down with it. | Screening before issuance, periodic re-screening, challenges on bulk creation from unverified accounts, and a revocation path fast enough that response time is measured in seconds. |
Sixteen decisions make up this architecture. Everything else across the twenty-three views is convention, sizing or consequence. Each record states the forcing question, the context that makes it hard, what was decided, how it is realised on Microsoft Azure, the options weighed, what the choice buys and costs, the conditions that would flip it, why it should still be right in ten years, and the lesson worth carrying to a different system.
Status of this document. This is a design, not a report on a running system. The rates, latencies, volumes, retentions and windows are the requirement's stated assumptions for a mid-size commercial link platform — 40,000 tenants, 12,000 branded domains, 12 billion stored links, 150,000 redirects per second — invented to be defensible and arguable rather than absent. They are to be replaced by measured telemetry before build, and four of them in particular are the first numbers real data should overturn: the 97% edge cache hit rate, which decides the cost per million redirects and the resolver fleet size; the 4% thirty-day working set, which decides whether the hot set is affordable at all; the 30-second edge cache TTL, which is simultaneously the hot-key absorber and the outer bound on the revocation promise; and the 18% bot share, which decides whether every customer-facing number is credible.
Where the writer's involvement ends, and how a change reaches a PoP on the other side of the world.
ADR-01The redirect plane reads a projection and never writes ADR-02Revocation travels on its own channel, ahead of the projection ADR-15Staleness bounds are published as contracts, not implied as immediacyHow a code is chosen, who guarantees it is unique, and why it can never be used twice.
ADR-03Codes are random, unguessable and seven characters — not counter-derived ADR-04A code is identity, and is retired permanently rather than reused ADR-05Alias uniqueness is the only strongly consistent operation ADR-06The code namespace is per domain, not globalWhere a lookup happens, what it is allowed to depend on, and what it returns.
ADR-07Resolution happens in twelve regional resolvers, not at the PoP ADR-08A 302 with a 30-second cache lifetime, never a permanent redirect ADR-09The link table is partitioned by code hash, not by tenantHow a click becomes a number somebody bills on, without a write on the redirect path.
ADR-10Clicks are collected from the edge access log, after the response has left ADR-11Full click events are shipped, with sampling reserved for pathological codes ADR-12Bots are classified and served, never blocked or silently countedScreening, terminal states, and the 12,000 domains the platform serves on customers' behalf.
ADR-13Destinations are screened before issuance and re-screened afterwards ADR-14Four distinct terminal states, owned by the platform ADR-16Branded domains and their certificates are the platform's responsibilityEvery capability on the twenty-three views, the Azure service chosen for it, the alternative that was weighed, and the reason. Azure was chosen for the package as a whole because it has appeared once in the practice's last ten deliveries while Google Cloud and AWS have carried three each — and because the topic's hard parts land on Azure primitives without a managed product making the architectural decisions: a globally replicated point-read store with native TTL, an anycast edge with per-domain managed certificates, and a high-cardinality append-only analytics store.
| Capability | Choice | Origin | Credible alternative | Why this one | Record |
|---|---|---|---|---|---|
| Edge termination and caching | Azure Front Door Premium — anycast, WAF, rules engine, 30 s response cache | Azure | Traffic Manager plus regional gateways | One anycast address set serving the shared domain and 12,000 branded domains with managed certificates, plus a response cache that carries the assumed 97% of traffic. DNS-based steering could not provide either. Also shaped by ADR-08, ADR-16. | ADR-07 |
| Branded domain certificates | Front Door managed certificates with Azure DNS verification | Azure | Customer-supplied certificates in Key Vault | At 12,000 domains, renewal is the availability risk. Managed issuance removes 12,000 expiry dates the platform would otherwise chase; customer-supplied certificates remain available for the enterprise tier that insists. | ADR-16 |
| Resolver compute | Azure Container Apps, twelve regions, KEDA-scaled on requests per second | Azure | Azure Functions on a consumption plan | Resolution is a warm, connection-heavy, latency-sensitive path with a 40 ms p99 budget; a container fleet holds that budget more predictably than per-request compute, and KEDA absorbs the assumed 4× burst. | ADR-07 |
| Link table | Azure Cosmos DB — single write region, read replicas in all read regions, native TTL | Azure | Azure SQL with geo-replication | The access pattern is a point read on a compound key at enormous cardinality, with expiry as a data property. Cosmos gives per-partition single-digit-millisecond reads, a change feed for propagation, and TTL for expiry in one component. Also shaped by ADR-09. | ADR-01 |
| Alias uniqueness | Cosmos DB conditional create (If-None-Match) in the write region | Azure | Multi-region writes with last-write-wins | A conditional create is a genuine compare-and-set. Multi-region writes would make the platform's one zero-tolerance guarantee probabilistic. | ADR-05 |
| Hot set cache | Azure Cache for Redis, zone-redundant, one per read region | Azure | Cosmos DB integrated cache | An explicit cache with measurable hit rate, eviction metrics and a pre-warm path; the integrated cache is easier and gives less control over exactly the number the cost model depends on. | ADR-07 |
| Revocation propagation | Azure Event Grid topic per region, 5 s pull into an in-process set | Azure | The same change feed as the projection | The 60-second bound needs a channel carrying only identifiers. Sharing the projection's path would couple a safety promise to the throughput of routine edits. | ADR-02 |
| Projection propagation | Cosmos DB change feed into a per-region projection worker | Azure | Dual-write from the management plane to each region | The change feed is replayable and ordered per partition, which is what makes the projection rebuildable in 30 minutes. Dual-write makes the writer responsible for twelve regions' availability. Also shaped by ADR-15. | ADR-01 |
| Click collection | Front Door access logs to Azure Event Hubs, 7-day retention | Azure | Resolver-emitted events | The log covers cached and uncached clicks alike, which is the only way a 30-second cache and an exact click count can coexist. Also shaped by ADR-11. | ADR-10 |
| Click analytics | Azure Data Explorer — raw 90 days, materialised views for rollups | Azure | Synapse or a lakehouse | High-cardinality append-only time series with interactive per-code queries is exactly ADX's shape; materialised views give rollups without a separate batch pipeline. | ADR-11 |
| Management plane | Container Apps in two regions, active-standby | Azure | Active-active in twelve regions | The write path is bound to a single write region anyway (ADR-05), so more management regions would add cost without adding availability. Also shaped by ADR-05. | ADR-01 |
| Screening and publishers | Azure Functions, event-driven | Azure | Long-running workers on the management plane | Screening and re-screening are bursty and idempotent; the periodic re-screen is a scheduled fan-out with no state of its own. | ADR-13 |
| Reputation verdicts | Three third-party URL reputation feeds | Third party | A single provider, or in-house classification | Three providers mean no single vendor outage or false-positive run decides the platform's behaviour; in-house classification is a different company's product. | ADR-13 |
| Identity | Microsoft Entra ID with workload identity; tenant-scoped API keys hashed in Key Vault | Azure | Connection strings and a self-managed key store | No credential in configuration anywhere, and key revocation without a redeployment. The resolver holds a read-only workload identity and nothing else. | ADR-01 |
| Audit and abuse evidence | Append-only store with an immutability policy — 400 days and 2 years | Azure | Rows in the operational database | An audit record that the application can update is not evidence, and an abuse decision has to be defensible after the customer has escalated. Also shaped by ADR-14. | ADR-13 |
| Observability | Azure Monitor for resources, ADX for the promise probes | Azure | Resource monitoring alone | Component health would have stayed green through every failure this platform actually fears. The wrong-destination and revocation probes are queries over ADX, alarmed on the published promise. Also shaped by ADR-05. | ADR-02 |
| Infrastructure and delivery | Bicep modules with GitHub Actions and OIDC federation | Azure + GitHub | Terraform with a service principal secret | Twelve read regions have to be identical by construction, and OIDC removes the last long-lived secret from the pipeline. | ADR-01 |
Does serving a click touch the authoritative link table, or a derived copy of it that the click path cannot modify?
Does 'stop serving this code' use the same propagation path as 'this code now points somewhere else'?
Does the platform tell customers how long a change takes to take effect, or let them assume it is instant?
Where does a generated code come from: a random draw, an encoded counter, a pre-allocated block, or a hash of the destination?
When a link is deleted or expires, may its code be issued again to someone else?
How is `(domain, code)` guaranteed unique when creation happens concurrently, and what consistency does the rest of the system get?
Is `spring` one alias across the whole platform, or one per domain?
Does the lookup run at the edge against a replicated dataset, or in a regional service the edge forwards to?
What status code does a redirect carry, and how long may a browser or intermediary remember it?
What is the partition key of the link table: the tenant, or the code itself?
What records a click: the resolver that served it, or the edge that logged it?
Does the platform ship one event per click, or aggregate counters at the edge and ship rollups?
What does the platform do with the assumed 18% of traffic that is unfurlers, scanners and prefetchers?
Is a destination checked once at creation, continuously, or not by the platform at all?
What does a code return when it is unknown, expired, deactivated or blocked — and whose page is it?
Who owns verification, certificate issuance and renewal for 12,000 customer-owned domains?
Eleven terms that mean something specific in this package. Where a term is used loosely elsewhere in the industry, the definition here is the one the twenty-three views were drawn against.
| Package | What it is | What it does here | Considered instead |
|---|---|---|---|
| Code | The short string after the host — the `spring` in `go.acme.com/spring`. | Permanent identity. Issued once, never reassigned, never reissued after retirement. | Treating it as a lookup key that can be recycled, which eventually resolves a printed QR code to a stranger's site. |
| Destination | The URL a code currently resolves to. | Mutable state. Changing it is the product's central feature, bounded by the five-minute propagation promise. | Treating the destination as part of the link's identity, which makes every campaign change a new code and defeats the point of shortening. |
| Projection | The read-optimised, eventually consistent copy of the link table that the redirect path actually reads. | Disposable. Rebuildable from the link table in 30 minutes with no operator input. | Calling the hot set a cache in front of the database, which invites somebody to read through it and then write back. |
| Deny-set | A compact set of codes that must stop resolving now, propagated on its own channel and checked before any cache layer. | The mechanism behind the 60-second revocation promise. | Relying on cache invalidation, which couples the safety promise to the slowest purge in the fleet. |
| Reservation | The claim record for `(domain, code)`, strongly consistent, which outlives the link it once identified. | The uniqueness authority and the retirement ledger in one. | Deriving uniqueness from the link table, which frees a code the moment its link is deleted. |
| Terminal state | One of four distinct non-redirect outcomes: unknown (404), expired (410), deactivated (410), blocked (interstitial). | What a clicker can act on. Never collapsed into a single generic error. | One 404 for all four, which turns every dead link into a support ticket and every blocked link into a mystery. |
| Traffic class | The classification of a request as human, unfurler, scanner or unknown, decided at the edge. | Excluded from customer-facing counts and tenant quotas, retained in the raw data so a reclassification can be replayed. | Blocking bots, which breaks link previews in every chat client and mail scanner the platform's customers rely on. |
| Edge hit rate | The share of clicks answered by the 30-second response cache without reaching a resolver. | The dominant lever on both tail latency and cost per million redirects; assumed 97%, alarmed below 94%. | Treating it as a performance metric only, when it is the main line on the bill. |
| Propagation bound | The published maximum time for a change to take effect globally — 60 s for a revocation, 5 minutes for an edit. | A contract. Support answers, customer expectations and abuse response all derive from it. | Implying immediacy, which makes every support conversation a negotiation and every abuse response unaccountable. |
| Working set | The share of stored links that receive a click in a 30-day window; assumed 4%. | What makes a hot set affordable against 12 billion stored links. | Sizing the cache against the table, which prices the architecture out before it is built. |
| Click | One redirect served, recorded from the edge access log after the response has left. | The billing and reporting unit, reconciled daily to ±0.5% with a 0.05% loss budget. | A counter incremented by application code, which makes caching and accurate counting mutually exclusive. |
svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.