Table of Contents
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
}
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.
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.
To simplify the declaration of IDs, we’ll introduce a macro, id!
, which does the heavy job of:
::new()
method on useful functions on every ID type such as::new()
,Uuid
, such as Deref
, From
, etc.