Grokking the System Design Interview, Volume II
Vote

0% completed

Unique ID Generator: High-Level Design

Step 5: High-Level System Design

First, a decision: a library or a service?

The service itself

Step 5: High-Level System Design

First, a decision: a library or a service?

Snowflake needs no coordination to make an id. A node that knows its own machine number can produce ids forever without talking to anything. So the obvious question is why there is a service here at all.

There are two ways to ship this.

As a library, running inside each client. The application imports a package, gets a machine number at startup, and makes ids in its own process. This is what Twitter, Instagram and Discord actually do. An id costs a few processor instructions and no network at all.

As a service, called over the network. Clients ask for ids over HTTP or gRPC. This is what the rest of this chapter builds.

On these measures, the library is clearly better.

LibraryService
Cost of one ida few instructionsa network round trip
Added latencynone worth measuring1 to 5ms in region
New failure modenoneevery insert now depends on another service
Machine numbers neededone per processone per service node

A service puts a network hop in front of every row a client inserts. It also makes id generation a shared dependency, so an outage there stops writes everywhere. Those are real costs, and a candidate who proposes a service without naming them has missed the interesting part of this question.

So why build one anyway? Three reasons, and they are about organizations rather than machines.

  • Machine numbers are the hard part. Ten bits give 1024 of them. Handing one to every process of every service, across deploys and autoscaling, is a coordination problem that grows with the fleet. A service concentrates that problem into a few dozen long-lived nodes.
  • Clients are written in many languages. A library has to be built and kept correct in each one. Subtle bugs in clock handling then exist in several places at once.
  • The format has to change eventually. Moving from 41 bits of time to 42, or changing the epoch, means redeploying every client that carries the library. Behind a service it is one rollout.

The best answer is a hybrid, and it is the one to say out loud. Ship a library for the services that generate ids in bulk and can be trusted with a machine number. Run the service for everything else, and for clients in languages the library does not cover. The count parameter from Step 4 is what makes the service tolerable: a client that fetches a thousand ids at a time makes one network call per thousand rows rather than one per row.

The rest of this step designs the service, because it is the harder of the two and it contains the library as a part.

The same algorithm, shipped two ways. The library is cheaper on every axis except the ones that are about organisations rather than machines.
The same algorithm, shipped two ways. The library is cheaper on every axis except the ones that are about organisations rather than machines.

The service itself

Architecture Overview: We will build a dedicated ID Generation Service that runs in multiple regions and multiple instances for scalability. Here are the main components and their interactions in our design:

  • Client Services: These are the various microservices or applications that need unique IDs (for new database records, objects, etc.). Instead of generating IDs themselves, they will call our ID generator service whenever they require a new ID.
  • API Gateway / Load Balancer: Clients call a well-known endpoint for the ID service. A global load balancer or API gateway will route requests to an ID generator instance. This could be done via DNS (directing to a regional endpoint) or via an anycast IP or cloud load balancer that finds a nearby healthy instance. The gateway ensures even load distribution and directs traffic to the closest datacenter for low latency.
  • ID Generation Service Nodes: These are the worker nodes (servers or containers) that actually create IDs. They run our ID generation algorithm (which we'll detail in the next section). We will have multiple instances per region, each aware of a unique node ID (so they don't produce colliding IDs). They are essentially stateless in that any node can handle any request - no persistent per-request data - but each has some configuration (its node identifier and algorithm state). The service is likely implemented as a lightweight service in a high-performance language (Java, Go, C++, etc.), optimized for fast atomic operations and time retrieval.
  • Coordinator for Node IDs: To guarantee uniqueness across datacenters and nodes, we'll use a small coordination service. A coordination component (such as Apache ZooKeeper or etcd or a lightweight consensus service) will assign each generator node a unique identifier (consisting of region ID and machine ID bits). For example, when a generator node starts up, it contacts the coordinator to obtain a free "worker ID". The coordinator keeps track of which IDs are in use to avoid duplication. This ensures no two live nodes ever use the same ID space. (If the coordinator goes down temporarily, existing nodes continue using their IDs, but no new nodes can join - we'll run the coordinator as a robust cluster to minimize downtime.)
  • (Optional) Database for Segment Allocation: Note: In our final design we favor the Snowflake approach without a central DB. But if we were to support an alternative mode (segment-based ID blocks), a relational database or key-value store might be used to store and increment counters. In that mode, each request to the DB yields a range of IDs which the service can then hand out. This DB would be a critical component (and potential bottleneck), so in our chosen approach we avoid it except as a backup or hybrid solution.

Request Flow:

  1. Client Request: A client needing a new ID makes a request to the ID generation service's API (for example, an HTTP POST /v1/ids with type set to sortable or opaque). This request goes to the load balancer or API gateway.

  2. Routing: The load balancer forwards the request to one of the available ID generator nodes in the nearest region (using health checks and possibly considering client location). For global clients, DNS might resolve to a local region's endpoint. For a truly global service, we might also allow cross-region calls if one region is overloaded (though normally each region can handle its local traffic).

  3. ID Generation: On the chosen generator node, the service code checks the request type:

    • If time-sortable ID is requested, the node will use the Snowflake-like algorithm: it reads the current timestamp in milliseconds, and combines it with its configured region/machine ID and an internal sequence counter to generate the next ID. (Detailed algorithm in next section.)
    • If opaque ID is requested, the node will use a random ID generator: e.g. call a secure random number generator to produce 128 bits, or use a library call to generate a UUIDv4. This does not require any shared state (each call is independent). In either case, the computation is local and very fast - just a couple of arithmetic and bit operations or random bytes generation. There is no additional network call on this critical path (the service node doesn't need to ask any other node to get an ID in our design).
  4. Response: The service node returns the generated ID to the client, typically as a numeric string (for time-sortable) or a UUID-formatted string for opaque. For example, it might return {"ids": ["1541757210912000000"]} or {"ids": ["c2f3a8d0-7e5b-4efb-b1a2-5d91c1e315b8"]} depending on type. The client receives this response, and then uses the ID (e.g. as a key in their database insert). The whole round-trip is quick; even with network overhead, it should be well under the latency target from Step 2 - often a few ms within the same region.

  5. Failure Handling: If the request fails (e.g. the chosen node was unresponsive), the client or gateway can retry, potentially hitting a different node. The ID service is idempotent in the sense that a retried request will just produce a new ID (there's no harm in unused IDs - an ID that was generated but not used is just lost, with no conflict). Therefore, retries are safe. Clients could even request a batch of IDs in advance so spare IDs are ready in case of transient issues (this is an optional client-side optimization).

Multi-Region Uniqueness: We ensure uniqueness across regions by assigning a distinct region identifier to each region's cluster of ID nodes. This region ID forms part of the generated ID (for time-sortable IDs) or influences the ID (for opaque, we might not include it explicitly, but we can simply rely on randomness being globally unique). For the Snowflake IDs, for example, we use some of the machine-id bits to encode the region. For instance, a 10-bit machine ID could be split into a 5-bit datacenter ID and 5-bit host ID. Thus, Region A might be datacenter 5 and Region B datacenter 6, etc. This means even if two machines in different regions generate an ID at the exact same millisecond with the same sequence number, the datacenter portion will differ, making the 64-bit IDs distinct. The coordinator service can manage this by pre-allocating a range of machine IDs to each region (e.g. region 1 gets IDs 0-31 for its nodes, region 2 gets 32-63, etc., if using 5-bit region). Alternatively, the region ID can be explicitly one field in the ID. In any case, no coordination between regions is needed per request - the design inherently prevents overlap.

Example Flow: Suppose Service X in the US-East region needs a new ID for a user profile creation. It calls the ID API, the US-East load balancer routes to an ID node (with, say, machine ID = region 1, node 3). The node's Snowflake algorithm takes the current wall-clock time and turns it into a 41-bit value counted from the epoch, combines it with its datacenter=1 and worker=3 bits, and sequence (say 5 if it already made 5 IDs this millisecond), producing a 64-bit ID like 0x1E2C... (some big integer). This is returned in, say, decimal form. Meanwhile, a client in EU region might be calling nearly simultaneously; their ID node has datacenter=2, maybe sequence 1, etc., yielding a different 64-bit number. Even if the timestamp part was identical, the datacenter bits differ so the IDs are unique. Later, when these IDs are stored in a database, sorting them will put the US-East one before or after the EU one depending on timestamp (if timestamps were truly equal to the ms, their relative order in numeric comparison will be determined by datacenter bits - so not strictly by true time, but such ties are extremely rare and only within the same millisecond window).

In summary, the high-level design uses distributed ID generator nodes that operate mostly independently, coordinated only by a lightweight service to assign unique node IDs. Clients interact with it through a simple network API. This design is stateless in the request/response path, enabling easy load balancing and horizontal scaling.

Next: Step 6, which compares the ways to actually make an ID.

On This Page

Step 5: High-Level System Design

First, a decision: a library or a service?

The service itself