Table of Contents

Identifiers

Entities need a way to be uniquely identified when referenced in business logic. Many applications default to using the Uuid type, while some revert to integers or strings. The core of this article isn’t really about what ID type to use; rather how to represent them in your application such that there’s a little likelihood of mixing up IDs during assignment.

<aside> <img src="/icons/light-bulb_yellow.svg" alt="/icons/light-bulb_yellow.svg" width="40px" /> For the rest of this article, we’ll strictly adopt Uuid as the type we’ll use.

</aside>

Let’s start with a dead-simple example of the Uuid type:

struct User {
	id: Uuid,
	username: String,
}

struct Organization {
	id: Uuid,
	name: String
}

The Problem

Let’s take a fundamental example where we attempt to assign a user to an organization:

fn sign_up() {
	let user = User::mock(); // Assume this function exist
	let organization = Organization::mock(); // Assume this function exist
	assign_to_org(organization.id, user.id);
}

fn assign_to_org(user_id: Uuid, organization_id: Uuid) { ... }

Did you notice anything wrong with the above code? We wrongly assigned the wrong arguments to the assign_to_org(...) but never got flagged. In this case, it becomes the absolute responsibility of the code author to ensure the correct pass in the write variables.

For small codebases, it’s very easy to avoid such simple footguns. However, with growing codebases, where entities could be slightly related in terms of naming, it’s much more common and accessible to make such mistakes. You want to reduce the chances of this happening to the barest minimum.

Proposal

Compiled languages already provide good guarantees for problems like this. With type checking available, they ensure that the wrong types can’t be misassigned. If this condition is violated, a compiler error is thrown.

The way around that is to introduce a dedicated ID type - similar to a Uuid but as a distinctive type—a type that wraps around a Uuid but with all the characteristics of a Uuid. Through its trait system, Rust provides valuable building blocks that allow a new type to behave like an underlying type.

Code

To simplify the declaration of IDs, we’ll introduce a macro, id!, which does the heavy job of: