CASE STUDY

Tagging System with Clean REST APIs (Confluence / Jira Labels)

2 min read·345 words·Beginner

Asked at

1 candidate report in Jan 2026

How to use this case study

SDE-2 / Mid

Design the tables (tags, entity_tags) and REST endpoints to create, rename, delete, attach and detach tags, and list tags for an entity.

SDE-3 / Senior

Support finding entities by tags (AND/OR), autocomplete, bulk attach, pagination and consistency when a tag is renamed or deleted.

Staff / Principal

Discuss scale (popular tags with millions of entities), tag counts, permissions, and search indexing.


0) Problem Restatement

Atlassian asked: design a tagging system (like labels on Confluence pages or Jira issues) with a strong focus on REST API design. Required: create a tag, rename a tag, delete a tag, attach or detach one or more tags to an entity (a page or an issue), list the tags of an entity, and find entities by tag.


1) Data Model

CREATE TABLE tags (
  tag_id     BIGINT PRIMARY KEY,
  workspace_id BIGINT NOT NULL,
  name       TEXT NOT NULL,
  name_norm  TEXT NOT NULL,          -- lowercase/trimmed for uniqueness and search
  created_by BIGINT, created_at TIMESTAMP,
  UNIQUE (workspace_id, name_norm)
);
CREATE TABLE entity_tags (
  tag_id      BIGINT REFERENCES tags(tag_id) ON DELETE CASCADE,
  entity_type TEXT NOT NULL,         -- 'page', 'issue'
  entity_id   BIGINT NOT NULL,
  tagged_by BIGINT, tagged_at TIMESTAMP,
  PRIMARY KEY (tag_id, entity_type, entity_id)
);
CREATE INDEX ON entity_tags (entity_type, entity_id);   -- tags of an entity
CREATE INDEX ON tags (workspace_id, name_norm text_pattern_ops);   -- prefix autocomplete

Entities reference tags by ID, so renaming a tag is a single-row update and every entity shows the new name automatically.


2) REST API

POST   /v1/tags                              { name }                       → 201 { id, name } | 409 exists
GET    /v1/tags?prefix=rel&limit=10                                          → autocomplete
PATCH  /v1/tags/{tagId}                      { name }                       → 200 | 409 name taken
DELETE /v1/tags/{tagId}                                                      → 204 (detaches everywhere)

GET    /v1/{entityType}/{entityId}/tags                                      → 200 [ tags ]
POST   /v1/{entityType}/{entityId}/tags      { tagIds: [..] } or { names: [..] }  → 200 (idempotent attach)
DELETE /v1/{entityType}/{entityId}/tags/{tagId}                             → 204

GET    /v1/tags/{tagId}/entities?type=page&cursor=&limit=50                  → entities with this tag
GET    /v1/search?tags=release,backend&match=all&type=issue&cursor=          → AND / OR search
POST   /v1/tags/bulk-attach                  { tagIds, entities: [...] }    → 202 job for large batches

Design choices to call out:

  • Resource nesting: an entity's tags live under the entity. Tags are their own top-level resource.
  • Idempotency: attaching an already-attached tag is a no-op (INSERT ... ON CONFLICT DO NOTHING), so retries are safe. Deleting a missing link returns 204 or 404 consistently (document which).
  • Attach by name: creates missing tags automatically (with the same normalization), which is convenient for UIs.
  • Status codes: 201, 200, 204, 400 (invalid name), 403 (no permission on the entity), 404, 409 (duplicate name).
  • Pagination: cursor-based for entity lists.
  • Validation: name length, allowed characters, and a max number of tags per entity.

Architecture Diagram

flowchart LR
    UI["Web / API clients"] --> API["Tagging API"]
    API --> DB[("tags + entity_tags")]
    API --> PERM["Permission check on entity"]
    DB -->|"changes"| IDX["Search index update"]

3) Finding Entities by Tags

  • OR (any of the tags): SELECT DISTINCT entity_id FROM entity_tags WHERE tag_id IN (...).
  • AND (all tags): ... GROUP BY entity_id HAVING COUNT(DISTINCT tag_id) = N.
  • At large scale, index tags in the search engine alongside entity content, so tag filters combine with text search and permissions.


4) Wrap-Up

Store tags per workspace with a normalized unique name, and link them to entities through an entity_tags join table (a composite primary key, indexes both ways), so renames are one-row updates and deletes cascade. Expose clean REST resources: tags as a top-level collection (create, rename, delete, prefix autocomplete), and tags nested under entities with idempotent attach/detach, plus by-tag queries with AND/OR, cursor pagination, bulk async attach, permission checks and clear status codes.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →