The system design question
Design a system that lets people add tags to content across Atlassian products, like Jira issues and Confluence pages. Users should see the tags on any item, and find everything with a given tag.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Adding and removing tags | Full-text search |
| Showing an item's tags | Tag suggestions |
| Finding everything with a tag | The products' own pages |
| Popular tags | Billing |
Then agree on what "good" means:
- Fast both ways: showing an item's tags and listing a tag's items should both be quick.
- Safe: people should never see content they don't have access to.
- Quick to update: a new tag should show up within a second or two.
- Separate: each customer's tags stay in their own space.
Rough numbers (5 minutes)
Made up, but reasonable: 1 billion taggable items across all customers, about 3 tags each, and far more people reading tags than adding them.
- 3B
- item-to-tag links
- ~300 GB
- to store them, at about 100 bytes each
- ~2K
- tag changes per second
- ~50K
- tag look-ups per second
| Question | Rough math | What it means |
|---|---|---|
| How big is one huge tag? | A tag like 'bug' on a big site | Could be millions of items, so we'll need pages |
| Does it fit on one machine? | 300 GB, stored twice | Possible at first, but we'll want to split it up |
| Reads or writes? | 50K reads vs 2K writes | Read-heavy, so caching helps |
The overall design (10 minutes)
- A product, like Jira, asks the tag service to add or remove a tag on one of its items.
- The tag service saves the link and records the change.
- Showing an item's tags or a tag's items reads straight from the tag database.
- Each change goes on a queue, and a background job keeps tag counts and popular tags up to date.
- Before showing results, we check the person can actually see each item.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Add or remove a tag | Product | Customer site, item ID, item type and tag |
| Get an item's tags | Product | Customer site and item ID |
| Get a tag's items | Product or user | Customer site, tag, and a cursor for the next page |
| Popular tags | Product | Customer site, and optionally a product |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Tags | Relational database | A small list of names per customer site |
| Item to tags | Database, keyed by item | Fast 'what tags does this have?' |
| Tag to items | Database, keyed by tag | Fast 'what has this tag?' |
| Tag counts | Cache, rebuilt from changes | Read constantly, fine if slightly behind |
Step 1: The data model (10 minutes)
Store every link twice: once organized by item, once organized by tag. That way both look-ups are a quick read. It costs some extra space and two writes per change, but tags change far less often than they're read.
Put the customer site in every key. Every query starts with the site, so one customer's data never mixes with another's, and later it gives us an easy way to split the data across machines.
A relational database is a fine start. If it gets too big, a wide-column store suits these 'give me everything under this key' look-ups well.
Step 2: Very popular tags (10 minutes)
A tag like 'bug' can be on millions of items, so never return them all at once. Return a page at a time, and use a cursor that says where the next page starts.
If one tag gets too big for one place, split its items into a few buckets and read from each in turn.
Counting a tag's items on every request is too slow. A background job reads the change queue and keeps running counts in a cache, like stream processing. Popular tags are read a lot, so caching them helps too.
Step 3: Permissions (10 minutes)
Each product owns its own permissions, and they change all the time. Copying them into the tag service is a good way to end up showing someone a page they shouldn't see.
So fetch a page of results, ask the product which ones the person can see, and drop the rest. If too many get dropped, fetch a bit more.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Storing links twice | One table with an index | Both look-ups stay fast as things grow | Writes became the bottleneck |
| Checking permissions when reading | Copying permissions into the tag service | Products stay the source of truth | Filtering threw away most results |
| Counts kept by a background job | Counting on every request | Counting millions of items is slow | Exact counts became a requirement |
| Splitting by customer site | Splitting by tag | Every query starts with the site | One customer got too big for its share |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| The change queue falls behind | Tags still work. Counts and popular tags are just a bit out of date |
| One of the two writes fails | Retry it, and a repair job compares the two copies and fixes any differences |
| Content gets deleted in a product | The product sends an event, and we remove its tags |
| A product's permission check is slow | Time out and show fewer results, never unchecked ones |
| Someone retags 100,000 items at once | Run it as a background job, in batches |
Follow-up questions (5 minutes)
How would you rename or merge tags?
Give tags IDs and store links by ID, so renaming is one change. Merging is a background job that moves links over in batches.
How would you suggest tags?
Look at tags on similar items in the same space, and popular tags in that project.
Could users follow a tag?
Read the change queue and notify followers, batching notifications so nobody gets spammed.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–30 | The data model |
| 30–40 | Very popular tags |
| 40–50 | Permissions |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
- Caching strategiesMake reads faster, and figure out what to do when data changes.
- Stream processing and windowingCount things in data that never stops coming in.
- Message queues and event streamsKeep services from depending on each other, and handle traffic spikes.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Atlassian.