tinyurl.com/abc123).1. Create short URL
POST /api/shorten{ "longUrl": "https://example.com/very/long/path" }200 OK: { "shortUrl": "https://tiny.url/abc123" }400 invalid/malformed URL, 429 rate limit exceeded2. Redirect to original URL
GET /{shortCode}302 Found with Location: https://example.com/very/long/path404 short code not found, 410 link expired/disabledEntry layer: An AWS Application Load Balancer (ALB) sits at the edge. It terminates TLS, health-checks service instances, distributes traffic across them, and routes by path: POST /shorten → Shortener Service, GET /{shortCode} → Redirect Service. ALB is chosen because it natively supports path-based routing and automatic failover, so no single server is a point of failure.
Write path (shorten URL): The Shortener Service (a stateless service, horizontally scalable) receives the long URL, requests a unique identifier from the ID Generation Service, and stores the mapping shortCode → longURL as a key-value record in DynamoDB. DynamoDB is chosen because every redirect is a single-key lookup — it serves these in single-digit milliseconds, scales horizontally without manual sharding, and has built-in TTL support for expiring links. The short URL (e.g., tinyurl.com/abc123) is returned to the client.
Identifier generation: Short codes come from a Snowflake-style generator — 41-bit timestamp + 8-bit server ID + 14-bit sequence counter — base62-encoded into an ~11-character code. Because each server's ID is unique, no two servers can produce the same code, so uniqueness is guaranteed with no collision-check retry loop. The time-based component also makes codes non-sequential, so users can't enumerate or guess other short links.
Read path (redirect): The Redirect Service handles GET /{shortCode}. It first checks Redis:
The client receives a 302 Found response with the long URL in the Location header; the browser follows it to the destination. (302, not 301, so every click hits our service — enabling click analytics and letting us update or disable links instantly.)
Caching layer: Redis runs in cluster mode — keys are sharded across nodes, with replicas for read scaling and automatic failover. A cache outage degrades to DB reads but never takes the system down.
Storage layer: DynamoDB stores the shortCode → longURL mapping, partitioned on hash(shortCode). Since codes are random, traffic spreads evenly across partitions; a viral link's reads are absorbed by Redis so the database never sees the spike.
Scaling & availability summary: All services are stateless → scale by adding instances behind the ALB. DynamoDB scales automatically. Redis scales via cluster mode. The ALB health-checks and routes around dead instances, and Redis replicas handle node failure — giving high availability without a single point of failure.
Security: All traffic is HTTPS-only (TLS 1.3), terminated at the ALB — data is encrypted in transit end to end. Data at rest is encrypted via DynamoDB's server-side encryption (AES-256 with AWS KMS); Redis data is encrypted the same way. DDoS protection comes from AWS Shield (absorbs volumetric attacks at the network edge) plus AWS WAF filtering malicious patterns (SQLi, XSS) before requests reach the ALB.
1. ID Generation Service
Avoiding collisions under concurrency: IDs come from a Snowflake-style generator: 41-bit timestamp + 8-bit server ID + 14-bit sequence counter, base62-encoded into an ~11-character code. Because the server ID is unique per instance, no two servers can produce the same (timestamp, server ID, sequence) triple — uniqueness is guaranteed with no collision-check retry loop, even under heavy concurrent creates.
Hard to guess: Codes are time-derived and base62-encoded, so they're non-sequential and opaque. A user cannot enumerate or predict valid short links — attempting to scan the space is infeasible.
Generator outage / split-brain: Each instance runs its own generator with a server ID assigned at startup. If one generator dies or is partitioned, the remaining instances keep issuing unique IDs — there is no central generator to fail, so no split-brain window exists.
2. Caching Layer
Cache-aside pattern: The read path checks Redis first. On hit, the long URL returns in <1ms. On miss or TTL expiry, the Redirect Service queries DynamoDB, serves the redirect, and populates Redis — so the next request is served from cache.
TTL & eviction: Each entry gets a 24-hour TTL. Viral links refresh their TTL on every hit, so hot entries stay cached; cold links expire naturally, preventing memory bloat without manual eviction logic.
Purging on update/disable: When a link is edited or disabled, the service actively deletes the key from Redis so changes take effect immediately — no stale redirects.
Viral link protection: Redis absorbs the read spike for a suddenly popular link, so the database never sees the full load. For extreme virality, a CDN edge layer can cache the 302 response at the network edge.
3. Partitioning (Database Sharding)
Single-shard lookups: The database is partitioned on hash(short_code). Every redirect routes to exactly one shard — no scatter/gather, so latency stays consistent at any scale.
Even spread + hotspots: Because short codes are random, traffic distributes evenly across shards. A viral link is served by Redis/CDN before reaching the DB, preventing any single shard from being overwhelmed.
4. Rate Limiting
Mechanism: A distributed token bucket runs at the API gateway, backed by Redis. Each client (keyed by IP or API token) has a bucket — e.g., 100 tokens/min for creates, 1,000 tokens/min for redirects. Every request consumes a token.
Burst handling & backoff: Bursts are smoothed by the refill rate. When a bucket is empty, the gateway returns 429 Too Many Requests with a Retry-After header so clients back off automatically.
Management: Because the bucket state lives in Redis, all gateway instances share it — a client can't bypass limits by hitting different instances. Limits are configurable per tier (anonymous vs. registered users), adjustable at runtime without redeploy, and monitored for anomalies (e.g., a client suddenly spiking).
5. Scaling & High Availability
Scaling: Services are stateless → scale horizontally behind the ALB. DynamoDB scales automatically via partitioning. Redis scales via cluster mode (keys sharded across nodes, replicas for read scaling).
High availability: Services run across multiple availability zones; the ALB health-checks and routes around dead instances. DynamoDB replicates synchronously within the region (immediate durability) and asynchronously to a standby region for disaster recovery. Redis uses replica sets with automatic failover — a cache loss degrades to DB reads, never downtime.
6. Security
In transit: HTTPS-only (TLS 1.3), terminated at the ALB.
At rest: DynamoDB and Redis use server-side AES-256 encryption via AWS KMS.
SQL injection: DynamoDB is NoSQL — there is no SQL to inject; all lookups are parameterized key-value gets. The gateway additionally enforces a strict character allowlist for short codes ([a-zA-Z0-9]), so malicious payloads never reach the data layer.
XSS: Short URLs are opaque identifiers, never rendered as content. On create, the service validates and sanitizes the long URL — rejecting javascript: URIs and non-http(s) schemes — so redirect targets are always safe. Responses never echo user input.
DDoS: AWS Shield absorbs volumetric attacks at the network edge; AWS WAF blocks known attack signatures (SQLi patterns, XSS payloads, bad bots); rate limiting prevents application-layer exhaustion.
ID Generation Service failure: The generator is stateless and runs as multiple instances behind the ALB, each with a unique server ID assigned at startup. There is no central state to lose.
Access control: Services use IAM least-privilege roles; admin APIs (create/update/disable) require authentication and audit-log every mutation for traceability. Secrets live in KMS, rotated regularly.
Redis caching layer failure:
7. Monitoring & Logging
Metrics: Every service emits metrics to CloudWatch/Prometheus — request rate, latency percentiles (p50/p99/p999), error rates (4xx/5xx/429), Redis cache hit ratio, DynamoDB throttled requests, ID generator health. Grafana dashboards visualize these per service and per endpoint.
Logging: Structured JSON logs carry a correlation ID propagated across the whole request path (ALB → service → Redis/DB), so a single redirect can be traced end to end. Redirect access logs (shortCode, timestamp, referrer, user agent) double as click analytics. Logs stream to a central store (CloudWatch Logs/ELK) with retention policies.
Tracing: Distributed tracing (AWS X-Ray) samples requests to pinpoint slow components — e.g., is latency coming from Redis or DynamoDB?
Alerting: SLO-based alerts: redirect p99 > 100ms for 5 minutes, 5xx > 1%, cache hit ratio < 90%, any DynamoDB throttling. These page the on-call engineer; a dead-man's switch detects if the alerting pipeline itself goes silent.
Anomaly detection: Traffic patterns are monitored for spikes (viral link or abuse) to trigger auto-scaling or WAF rule adjustments automatically.
Schema: The core table is URL_MAPPINGS:
id — BIGINT primary key (internal identifier)short_code — VARCHAR(10), unique — this is what clients requestlong_url — TEXT (stored as the canonical, sanitized destination)created_at — TIMESTAMPexpires_at — TIMESTAMP, nullable (for expiring/limited-time links)I keep the table denormalized — a single flat record per short code. No user/domain tables are joined at query time because every redirect is one self-contained lookup; splitting it apart would add joins to the hottest path in the system for zero benefit.
Index strategy: Redirects run WHERE short_code = ?, so short_code gets a unique index. This is the only hot lookup path, and the unique constraint does double duty: it's the lookup accelerator and the collision guard at the storage layer. A duplicate insert fails with ConditionalCheckFailedException instead of silently overwriting.
Storage engine: DynamoDB. The workload is a textbook fit:
GetItem — single-digit-ms latency, no joins, no scansexpires_at as the TTL attribute makes DynamoDB delete expired items automatically (~within 48h, at no read cost) — no cron jobs, no manual purgePartitioning: Partition key = short_code itself — DynamoDB hashes it internally. Every lookup is a single GetItem hitting exactly one partition (no scatter/gather). Because base62 codes are random, writes spread uniformly across partitions, so no single partition becomes hot. No manual sharding or rebalancing ever.
Consistency: Strong on creation — the unique index enforces that a short code is never duplicated; once the write commits, the mapping is durable. Eventual on redirect reads — a brief propagation delay after creation risks only a transient miss (one retry/404), never data corruption. For the 99.9% of reads that are already-popular links, this is invisible.
Replication & HA: DynamoDB replicates synchronously across 3 AZs within the region (immediate durability), with optional cross-region async replication as a disaster-recovery standby. Failover is automatic — no manual promotion, no downtime window.