Design a URL shortener
New to some of the words above? Open a quick recap. Each takes about two minutes.
Quick recapBack-of-the-envelope estimates· 2 min
Round hard and say the sums out loud. A day has about 86,400 seconds (call it 100,000), and a month about 2.6 million. So 1 million things a day is about 12 a second. Multiply by the size of one thing to get storage, and by the years you keep it.
Quick recapThe design interview in four steps· 2 min
- Requirements: what must it do, and how fast, big and reliable must it be? 2. Estimates: rough numbers for traffic and storage. 3. A simple design that works, drawn as boxes and arrows. 4. Scale it: find the part that breaks first, fix it, repeat. Say why at every step.
Quick recapLoad balancers· 2 min
A load balancer sits in front of several identical app servers and hands each request to one of them. If a server crashes, it stops sending traffic there, so the site stays up. It works best when the app servers are stateless: any server can answer any request, because nothing is kept on the server itself.
Quick recapCaching· 2 min
Quick recapReplication· 2 min
The primary database takes every write and copies it to one or more read replicas. Reads can go to any replica, so reads scale out. The copy takes a moment (replica lag), so a replica can briefly miss a brand-new row.
Quick recapPartitioning (sharding)· 2 min
When data is too big or too busy for one database, split it into shards by a key, like hash(key) % 4. Each
shard holds a quarter of the rows. Consistent hashing places keys on a ring, so adding a shard moves only a few
keys instead of almost all of them.
Quick recapQueues· 2 min
A queue holds messages until a worker takes them. The sender drops a message and moves on without waiting, so slow work happens in the background. If the worker is busy or down, messages wait in the queue instead of being lost.
See the problem
Section titled “See the problem”Your team shares dashboard links in chat every day. They look like this:
https://dash.example.com/reports/sales?region=emea&from=2026-10-01&to=2026-10-31&group=week&chart=bar&filters=channel%3AonlineThe link breaks across two lines, someone copies half of it, and nobody can read it out on a call. You’d rather send sho.rt/a7Kp2Qx. Build the service that makes those short links. Then make it survive success: millions of people opening them every day.
Define the task
Section titled “Define the task”What it must do
- Make a short link for a long one:
POST /links {"long_url": "..."}returns a code likea7Kp2Qx. - Redirect:
GET /a7Kp2Qxanswers302 Foundwith the long link in theLocationheader. - Nice to have: a custom alias (
sho.rt/summer-sale) and an expiry date.
How well it must do it
- Redirects are fast (a few milliseconds on our side) and almost never down. A broken short link is a broken email campaign.
- Reads far outnumber writes: about 100 redirects for every new link.
- Codes should be short, and hard to guess.
Estimates (from the brief, rounded hard)
| What | Sum | About |
|---|---|---|
| New links a second | 100 million a month ÷ 2.6 million s | 40 writes/s |
| Redirects a second | 40 × 100 | 4,000 reads/s |
| Links after 5 years | 100 million × 60 months | 6 billion |
| Storage after 5 years (500 bytes each) | 6 billion × 500 B | 3 TB |
Peaks run 2 to 3 times the average, so plan for about 10,000 redirects a second at busy times. Size the machines for the peak; storage grows with the average.
Pause and think
Section titled “Pause and think”Hints open one at a time:
Hint 1 of 3
The smallest version needs only somewhere to run code and somewhere to keep the links.
Hint 2 of 3
A database already gives every new row a unique number. Could that number be the code?
Hint 3 of 3
Redirects are reads, and the same popular links are read again and again. What is faster than a database for that?
Understand the idea
Section titled “Understand the idea”Think of a coat check. You hand over a long, bulky coat and get back a small numbered ticket. Later, the ticket gets you the coat. The coat room is v1. On a busy night, you add more staff (app servers) and hang the most-collected coats by the door (a cache). When the room fills up, you open more rooms, each for a range of tickets (shards).
A short code works the same way. Number each link, then write the number in base 62 (digits plus lowercase and uppercase letters). Seven characters give 62⁷, about 3.5 trillion codes, so they never run out.
"""Short codes: a whole number written with 62 symbols (0-9, a-z, A-Z)."""
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"INDEX = {ch: i for i, ch in enumerate(ALPHABET)}
def encode(n: int) -> str: if n == 0: return ALPHABET[0] out = [] while n > 0: n, rest = divmod(n, 62) out.append(ALPHABET[rest]) return "".join(reversed(out))
def decode(code: str) -> int: n = 0 for ch in code: n = n * 62 + INDEX[ch] return nWatch one redirect travel through each version, and the load on every part. The player stops to ask what happens next. Change the traffic below it to see when each version breaks.
v1: a few users
Parts:
- Client
- App server
- Database
Arrows:
- Client sends requests to App server
- App server sends requests to Database
Step 1 of 17. v1: one app server and one database. Each new link gets the next id, and the id written in base 62 is its short code.
Climb the ladder
Section titled “Climb the ladder”Each version is a working design. The code shows each one’s core as runnable Python, with simple fakes for the real parts. A dict stands for a key-value store, and queue.Queue for a message queue. Tests run every version.
v1: one server, one database
The idea: One app server and one relational database. The database's auto-increment id, in base 62, is the code.
Creating a link inserts a row, and the new id becomes the code: id 125 is 21. A redirect turns the code back into the id and looks it up by primary key, which is fast.
This is the right first answer in an interview. It works, it’s simple, and it gives you something to improve.
"""v1: one app server and one database. The database's id becomes the code."""
import sqlite3
from base62 import decode, encode
class Shortener: def __init__(self, db: sqlite3.Connection): self.db = db db.execute( "CREATE TABLE IF NOT EXISTS links (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " long_url TEXT NOT NULL)" )
def create(self, long_url: str) -> str: cur = self.db.execute("INSERT INTO links (long_url) VALUES (?)", (long_url,)) return encode(cur.lastrowid)
def resolve(self, code: str) -> str | None: try: link_id = decode(code) except KeyError: # a character that can't be in a code return None row = self.db.execute("SELECT long_url FROM links WHERE id = ?", (link_id,)).fetchone() return row[0] if row else None- Handles
about 2,000 redirects a second- Weak spot
one of everything- Why
- One app server handles about 2,000 requests a second; a crash of either box takes the whole service down.
Where it breaks: at our 4,000 redirects a second, the app server is overloaded twice over, and five times over at the 10,000 peak. It’s a single point of failure: one crash or one deploy and every link in every email is dead. And the codes count up (1, 2, 3…), so anyone can walk through every link anyone ever made, including private ones.
v2: load balancer, cache and read replicas
The idea: Put stateless app servers behind a load balancer, answer most redirects from a cache, and send the rest to read replicas.
The waste in v1: every redirect does the same database lookup, even for a link opened a million times today.
Size it for the peak: 10,000 redirects and 100 new links a second. The load balancer spreads them over 7 app servers: 10,100 ÷ 2,000 is just over 5, so 6 keep up, plus a spare. Each one asks the cache first, and about 9,000 a second end there. The other 1,000 read a replica, then fill the cache. New links go to the primary, which copies them to the replicas. If a brand-new link hasn’t reached the replica yet, the code asks the primary, so nobody sees “not found” for a link they just made.
301 or 302? A 301 Moved Permanently lets browsers remember the redirect and skip you next time. That saves traffic, but you can’t count those clicks or change where the link goes. Most shorteners answer 302 Found, so every click comes back to them.
"""v2: app servers behind a load balancer. Redirects try a cache, then a readreplica; new links go to the primary database."""
import sqlite3import timefrom collections.abc import Callable
from base62 import decode, encode
class TtlCache: """A small stand-in for Redis: each value is forgotten after `ttl` seconds."""
def __init__(self, ttl: float = 3600, clock: Callable[[], float] = time.monotonic): self.ttl = ttl self.clock = clock self.data: dict[str, tuple[str, float]] = {}
def get(self, key: str) -> str | None: hit = self.data.get(key) if hit is None: return None value, expires = hit if self.clock() >= expires: del self.data[key] return None return value
def set(self, key: str, value: str) -> None: self.data[key] = (value, self.clock() + self.ttl)
class Shortener: def __init__(self, primary: sqlite3.Connection, replica: sqlite3.Connection, cache: TtlCache): self.primary = primary self.replica = replica self.cache = cache
def create(self, long_url: str) -> str: cur = self.primary.execute("INSERT INTO links (long_url) VALUES (?)", (long_url,)) return encode(cur.lastrowid)
def resolve(self, code: str) -> str | None: long_url = self.cache.get(code) if long_url is not None: return long_url # most redirects end here try: link_id = decode(code) except KeyError: return None query = "SELECT long_url FROM links WHERE id = ?" row = self.replica.execute(query, (link_id,)).fetchone() if row is None: # A brand-new link may not have reached the replica yet. row = self.primary.execute(query, (link_id,)).fetchone() if row is None: return None self.cache.set(code, row[0]) return row[0]- Handles
about 100,000 redirects a second- Weak spot
one primary holds every link- Why
- App servers scale by adding more; the cache answers 9 in 10 redirects; the primary takes only the writes, 100 a second at peak.
Where it breaks: the data. 40 new links a second for 5 years is 6 billion links, about 3 TB, all in one primary. Backups, restores and schema changes on a database that size take hours. And the product now wants to count every click, which would add 4,000 writes a second, 10,000 at peak, to that same primary.
v3: shards, id blocks and a click queue
The idea: Split the links across key-value shards by their code, and hand out ids in blocks. Scramble them so codes don't count up. Count clicks later through a queue.
The waste in v2: one machine holds every link, and every write waits on it.
- Shards. A redirect is a lookup by one key, the code, so a key-value store fits.
shard_for(code)picks the shard. With 4 shards of under 1 TB each, every shard backs up quickly. Real systems use consistent hashing, so adding a shard moves only a few links. - Ids without a single counter. Each app server takes a block of 1,000 ids from a shared counter and hands them out from memory. Two servers never get the same block, so codes never clash, and the counter is asked once per 1,000 links. (A Snowflake-style id, built from the time, a machine number and a sequence, needs no counter at all.)
- Codes that don’t count up.
scramble()multiplies the id by a large number and wraps it around 62⁷. Every id still gets its own code, but neighbours land far apart, so guessing the next link doesn’t work. This hides the order; it isn’t security. Anyone who learns the multiplier can undo it. For private links, check who is asking on every redirect, or use truly random codes and check each new one for a clash. - Clicks off the redirect path. The app answers the redirect at once and drops a “click” message in a queue. A worker adds the clicks up in memory and saves the totals every few seconds: a handful of writes instead of 10,000 a second at peak.
- Also at this scale: a CDN or edge cache answers the most popular links close to users; rate limits and malware checks on link creation stop spammers; and copies in several regions keep redirects fast worldwide.
"""v3: links spread over many key-value shards, ids handed out in blocks, andclicks counted later through a queue, so a redirect never waits."""
import hashlibimport queuefrom collections.abc import Callable
from base62 import encode
CODE_LENGTH = 7CODE_SPACE = 62**CODE_LENGTH # about 3.5 trillion codes# About 0.618 of CODE_SPACE, so neighbouring ids land far apart. It shares no# factor with CODE_SPACE, so scramble() never gives two ids the same code.STEP = 2_176_477_521_915
def scramble(n: int) -> int: """Map 1, 2, 3, ... to codes that don't count up, so the next link is hard to guess.""" return (n * STEP) % CODE_SPACE
class IdBlocks: """Each app server takes ids 1,000 at a time from a shared counter, so it rarely asks the counter and two servers never hand out the same id."""
def __init__(self, next_block: Callable[[], int], size: int = 1000): self.next_block = next_block self.size = size self.next = self.end = 0
def take(self) -> int: if self.next == self.end: start = self.next_block() * self.size self.next, self.end = start, start + self.size n = self.next self.next += 1 return n
def shard_for(code: str, shards: int) -> int: """Which shard holds this code. Real systems use consistent hashing, so adding a shard moves only a few keys; plain % moves most of them.""" digest = hashlib.sha256(code.encode()).digest() return int.from_bytes(digest[:8], "big") % shards
class Shortener: def __init__(self, shards: list[dict[str, str]], ids: IdBlocks, clicks: queue.Queue): self.shards = shards self.ids = ids self.clicks = clicks
def create(self, long_url: str) -> str: code = encode(scramble(self.ids.take())).rjust(CODE_LENGTH, "0") self.shards[shard_for(code, len(self.shards))][code] = long_url return code
def resolve(self, code: str) -> str | None: long_url = self.shards[shard_for(code, len(self.shards))].get(code) if long_url is not None: self.clicks.put_nowait(code) # counted later; the user doesn't wait return long_url
def count_clicks(clicks: queue.Queue, counts: dict[str, int]) -> None: """The analytics worker: takes click messages off the queue until it gets None.""" while (code := clicks.get()) is not None: counts[code] = counts.get(code, 0) + 1- Handles
millions a second, by adding machines- Weak spot
many parts to run and watch- Why
- Every part grows by adding machines: more app servers, cache nodes and shards. The redirect path never waits for a click to be saved.
When v3 isn’t best: it’s many moving parts to run, watch and pay for. If you have 100 users, v1 on one machine with a backup is the right call. Build each step when the numbers say so, and say that out loud in the interview.
Draw it yourself
Section titled “Draw it yourself”Build each version on the board. Drag parts onto it, press the + on a part to draw an arrow, and press Check my design for plain-language feedback. Press Open full page for a bigger board. Step 2 asks you to size the app servers first. Your boards are saved in this browser.
Step 1: A small link shortener
- People paste a long link and get a short one.
- Opening the short link takes them to the long one.
- About 100 users a day. Keep it simple.
Solid arrow: sends requests toDashed arrow: copies data to
Watch the traffic
Send 10 requests through your board and see where each one goes. Then make a part fail and send them again.
How requests move
Requests start at the Client and follow your arrows. Reads try a cache first (4 in 5 are found there), then a read replica, then the database. Writes go to the database. Background jobs go on a queue, and a worker saves them. A load balancer skips a part that is down; nothing else can.
Compare
Section titled “Compare”| Version | Handles | Weak spot | Good | Use it when |
|---|---|---|---|---|
| v1 | about 2,000 redirects/s | One of everything; guessable codes | Simple, cheap, quick to build | A prototype, or a few thousand users |
| v2 | about 100,000 redirects/s | One primary holds all the data | Fast reads; survives a server crash | Read-heavy traffic, data under 1 TB |
| v3 | grows with machines | Many parts to run | Scales reads, writes and storage | Billions of links, clicks to count |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- The same long link twice: give it a new code each time (simplest, and each owner gets their own stats). Or look it up by a hash of the long link and reuse the code. Say which one you chose and why.
- Expiry: check the expiry date on each redirect and answer
404or410 Gonewhen it’s past. A background job deletes expired links later; don’t delete on the redirect path. - A viral link: one code gets 100,000 hits a second, all on one cache node. Keep extra copies of very hot keys, or let a CDN answer them.
- Cache stampede: when a hot link’s cache entry expires, thousands of requests miss at once and all hit the database. Let one request refill it while the others wait briefly, or refresh hot entries before they expire.
- Bad links: shorteners are a favourite tool for hiding phishing. Check new links against a blocklist, rate-limit creation, and let people report links.
- Codes that read as words: a random code can spell something rude. Some services remove vowels from the alphabet.
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- How many new links a day, and how many redirects? Is it read-heavy?
- How long do links live? Do they expire?
- Do we need custom aliases? Click analytics?
- How fast must a redirect be, and how bad is it if one is briefly down?
A 60-second walkthrough
“It’s read-heavy: about 40 new links and 4,000 redirects a second, 10,000 at peak, and 3 TB over 5 years. I’d start with one app server and one database, using the row id in base 62 as the code. That breaks at around 2,000 requests a second and has no spare. So v2 puts 7 stateless app servers behind a load balancer, sized for the peak. It adds a cache for hot links, since 9 in 10 redirects hit it, and read replicas behind that. I’d answer 302, not 301, so clicks keep coming back to us. When the data passes about 1 TB, v3 splits links across key-value shards by code. Each app server gets ids in blocks of 1,000, so codes never clash, and scrambles them so they can’t be guessed. Clicks are counted through a queue, so the redirect never waits.”
Likely follow-ups
- How long should the code be? 6 characters give 57 billion codes, enough for 6 billion links. 7 give 3.5 trillion, which leaves room and makes guessing a real code unlikely.
- How big is the cache? About 350 million redirects a day. Caching the links behind the busiest 20% is at most 70 million × 500 bytes, about 35 GB: the memory of one or two cache machines.
- An unknown or expired code? Answer
404 Not Found, or410 Goneonce a link has expired. Cache “not found” for a minute too, so someone trying random codes can’t flood the database. - Why not a hash of the long link? Cutting a hash to 7 characters makes clashes possible, so you need a check and a retry. Counter-based ids never clash.
- The counter is a single point of failure? Each server holds a block of 1,000 ids, so the counter can be down briefly. Run it as a small replicated service, or use Snowflake-style ids.
- A region goes down? Keep copies of the links in other regions; reads continue there. Writes can go to the nearest healthy region if codes carry a region part.
- Analytics? A queue and workers, with per-minute totals in a separate store, so counting never slows a redirect.
Red flags interviewers notice
- Jumping to shards and Kafka before saying what v1 is and why it breaks.
- No numbers: “it needs to scale” without traffic or storage estimates.
- A
301without knowing it hides clicks from you. - Saving each click in the database before redirecting.
- Generating random codes with no plan for clashes.
Practice
Section titled “Practice”Four related designs, from easy to hard. There’s no code to run: sketch each version, then compare with the answers. Say the numbers out loud as you go.
1. A pastebin (easy)
Section titled “1. A pastebin (easy)”People paste text, up to 10 MB, and get a short link to share it. 1 million pastes a day, each read about 10 times.
Hint 1 of 3
Start from the URL shortener. What is different when the value is 10 MB of text instead of a 100-byte link?
Hint 2 of 3
Big blobs and a database don’t mix well. Where do photo apps keep their photos?
Hint 3 of 3
Popular pastes are read again and again, by people all over the world.
Show the v1, v2 and v3 answers
Text in the database
The idea: Same as the shortener's v1, with the text in a column instead of a link.
1 million pastes a day at an average of 10 KB is 10 GB a day. That’s about 3.6 TB a year, all inside one database.
- Handles
small pastes, low traffic- Weak spot
huge rows slow the database- Why
- Every read pulls megabytes through the database, and backups grow with every paste.
Text in object storage
The idea: Keep the text in object storage (like S3), and only the code, the object's key and the expiry in the database.
Reads look up the key, then stream the text from storage. Writes upload first, then save the row, so a crash never leaves a row pointing at nothing.
- Handles
any size, steady traffic- Weak spot
every read still goes through your servers- Why
- Object storage is cheap per GB and built for big files; the database stays small and fast.
Plus a CDN and lifecycle rules
The idea: Serve pastes through a CDN, let storage delete expired pastes for you, and cap size and rate per user.
Pastes never change once made, which makes them perfect for caching: give them a long cache lifetime, and purge the CDN when a paste is deleted.
- Handles
worldwide, viral pastes- Weak spot
a cached paste can last a few minutes after a delete- Why
- The CDN answers popular pastes near the reader; lifecycle rules remove the need for a cleanup job.
2. Click analytics (medium)
Section titled “2. Click analytics (medium)”Show each link’s clicks per day, by country and device, on a stats page. 4,000 clicks a second, and viral links get far more.
Hint 1 of 3
What happens to a single row when 50,000 people a second try to add 1 to it?
Hint 2 of 3
The person clicking shouldn’t wait for the count. Who could do the counting later?
Hint 3 of 3
Do you need every click forever, or totals per link per day?
Show the v1, v2 and v3 answers
Add 1 on every redirect
The idea: UPDATE links SET clicks = clicks + 1 before answering the redirect.
A viral link means thousands of writes a second queued on one row. Redirects slow down exactly when the link matters most.
- Handles
a few clicks a second- Weak spot
one hot row, users wait- Why
- Every click is a write that locks the link's row, and the user waits for it.
A queue and a counting worker
The idea: Drop a click message in a queue and redirect at once; a worker adds clicks up in memory and saves per-minute totals.
The worker keeps a dict of (link, minute, country, device) → count and writes it out every few seconds.
- Handles
thousands a second- Weak spot
a worker crash loses the totals in its memory- Why
- The redirect does one fast enqueue; the database sees one write per link per minute.
An event log, rollups and replays
The idea: Write click events to a durable log (like Kafka). Add them up into daily totals in a column store, and keep the raw events for a while.
Delivery is at least once, so a crash can repeat events: give each click an id and skip ids already counted. Late events go into the day they happened, not the day they arrived.
- Handles
millions a second, new questions later- Weak spot
totals arrive a few seconds late- Why
- The log survives worker crashes and can be replayed; a column store answers 'clicks per day by country' fast.
3. Rate-limit link creation (medium)
Section titled “3. Rate-limit link creation (medium)”Stop one account from making more than 100 links an hour. Spammers script millions of requests.
Hint 1 of 3
The limit has to hold across all app servers, not on each one separately.
Hint 2 of 3
A counter per user per hour, kept in memory somewhere shared, is cheap to check.
Hint 3 of 3
With hourly windows, someone can make 100 links at 10:59 and 100 more at 11:00. How do you smooth that?
Show the v1, v2 and v3 answers
Count rows in the database
The idea: Before each insert, count this user's links from the last hour.
It works for a small site, but the check costs as much as the write it guards.
- Handles
low traffic- Weak spot
an extra query per write, and a race- Why
- Two requests can both count 99 and both insert: check-then-insert again.
A token bucket, per user and per IP
The idea: Each user has a bucket of 100 tokens that refills steadily; each link takes one. Check and update it atomically in one Redis script.
Answer 429 Too Many Requests with a Retry-After header, so well-behaved clients wait instead of sending again and again.
- Handles
smooth limits at any scale- Weak spot
one more service on the write path- Why
- Refilling over time removes the edge burst; per-IP limits catch scripts that make many accounts.
4. Custom aliases, worldwide (hard)
Section titled “4. Custom aliases, worldwide (hard)”People pick their own alias (sho.rt/summer-sale), and readers in Asia, Europe and the Americas all get fast redirects.
Hint 1 of 3
Two people in two regions ask for the same alias at the same moment. Who decides?
Hint 2 of 3
Generated codes and aliases must never collide. Can you keep them apart by their shape?
Hint 3 of 3
Reads can be served from any region; only creating an alias needs one place to agree.
Show the v1, v2 and v3 answers
One region, check then insert
The idea: All links live in one region; check whether the alias exists, then insert it.
Fine for a launch in one country.
- Handles
one region's users- Weak spot
slow far away; alias race- Why
- A reader on the other side of the world waits about 200 ms per redirect, and two people can take the same alias.
Unique alias, regional caches
The idea: A unique constraint decides who gets the alias; each region has read replicas and a cache, and writes go to the home region.
On a miss in a far region, ask the home region before answering “not found”, as v2 does with replica lag.
- Handles
readers everywhere- Weak spot
a new link reaches far regions a moment late- Why
- The database refuses the second alias, and nearby copies make redirects fast.
Separate shapes, one place to decide
The idea: Generated codes are exactly 7 base-62 characters; aliases must contain a hyphen or be another length, so they never collide. One region (or a consensus store) decides aliases; every region serves redirects.
If the deciding region is down, new aliases wait, but redirects and generated codes keep working everywhere.
- Handles
worldwide, with a region down- Weak spot
alias creation depends on one decider- Why
- Code creation stays local and fast; only the rare alias write crosses regions.
Check yourself
Section titled “Check yourself”A few quick questions. Ones you miss are remembered, so you can find your weak spots later.
Flashcards
Section titled “Flashcards”Add these to your review deck. Each card comes back a little later every time you get it right.
Cheat sheet
Section titled “Cheat sheet”- Estimate first: 100 million a month is about 40 a second. Multiply by size and years for storage.
- v1: one app server, one database, the row id in base 62. Say why it breaks.
- v2: load balancer, stateless app servers, a cache for hot links, read replicas. Answer 302, not 301.
- v3: shard by code, ids in blocks (or Snowflake), scrambled codes, clicks through a queue.
- Let a unique constraint settle races, and never make a redirect wait for analytics.
Go deeper
Section titled “Go deeper”- HTTP redirects on MDN: 301, 302, 307 and 308, and how browsers cache them.
- RFC 9110, section 15.4: the official meaning of each redirect status.
- Practice by building: the same three design steps on their own page.
- Next on the System Design roadmap: Rate-limiting algorithms and Unique ID generation.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started