Domain Entity Typed IDs
Goal
Choose the right level of type distinction for domain entity identifiers based on whether the project's type system is nominal or structural.
In a nominal type system, two types with the same structure are distinct if they have different names. A typed ID — a dedicated type per domain entity wrapping the underlying identifier — provides compile-time safety against mixing identifiers of different entities at no meaningful cost.
In a structural type system, two types with the same structure are interchangeable regardless of their names. A typed ID only provides safety if the language offers a low-friction mechanism to make the types nominally distinct. If achieving nominal distinction requires patterns that add significant boilerplate, ceremony, or ergonomic friction, use the underlying identifier type directly instead.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines the type of a domain entity's identifier
- introduces a new domain entity that needs an identifier type
- changes or refactors how a domain entity's identifier is typed
- reviews whether an identifier type provides appropriate type safety for the project's type system
The Rule
In nominal type systems, use typed IDs.
- Define a distinct type per domain entity that wraps the underlying identifier type.
- Use the language's idiomatic lightweight wrapper — inline classes, value classes, newtypes, or the equivalent.
- The typed ID ensures that identifiers of different entities are incompatible at compile time.
In structural type systems, assess friction before choosing.
- If the language offers a low-friction mechanism to make structurally identical types nominally distinct — and that mechanism does not add significant boilerplate to construction, serialization, or persistence — use typed IDs.
- If achieving nominal distinction requires patterns that are verbose, non-idiomatic, or poorly supported by the ecosystem — such as branded types that need manual casting, custom constructors, and special serialization handling — use the underlying identifier type directly.
When using the underlying type directly, rely on parameter naming for clarity.
- Name parameters and fields clearly —
customerId, orderId — so the intent is readable even without type distinction.
- Accept that the compiler will not catch accidental ID swaps in this case.
Do not mix approaches within the same project.
- All domain entity identifiers in a project must follow the same convention — either all typed IDs or all underlying type.
- Follow the project's established convention. If no convention exists, choose based on the rules above and apply consistently.
Detection Workflow
Determine the type system of the project's language.
- Nominal: Kotlin, Java, Scala, Rust, Swift, C#, Go, Haskell — types are distinct by name.
- Structural: TypeScript, Python, Elixir, Clojure — types are interchangeable if structurally identical.
Check the project's existing convention.
- Look for how existing domain entity IDs are typed.
- If a convention exists, follow it.
If no convention exists, apply the rule.
- Nominal type system → use typed IDs.
- Structural type system → assess whether a low-friction mechanism exists for nominal distinction. If yes, use typed IDs. If no, use the underlying type directly.
Writing or Changing Domain Entity ID Types
For nominal type systems — define a typed ID per domain entity:
// Kotlin — inline value class
@JvmInline
value class OrderId(val value: UUID)
@JvmInline
value class CustomerId(val value: UUID)
// Java — record
public record OrderId(UUID value) {}
public record CustomerId(UUID value) {}
// Rust — newtype
pub struct OrderId(pub Uuid);
pub struct CustomerId(pub Uuid);
// Swift — struct wrapper
struct OrderId: Hashable {
let value: UUID
}
struct CustomerId: Hashable {
let value: UUID
}
// C# — readonly record struct
public readonly record struct OrderId(Guid Value);
public readonly record struct CustomerId(Guid Value);
// Go — named type
type OrderId uuid.UUID
type CustomerId uuid.UUID
For structural type systems where typed IDs add friction — use the underlying type:
// TypeScript — use the underlying type directly
class Order {
readonly id: string
readonly customerId: string
}
// Python — use the underlying type directly
@dataclass(frozen=True)
class Order:
id: UUID
customer_id: UUID
For structural type systems where a low-friction mechanism exists — use typed IDs:
// TypeScript with a library like ts-brand or a project convention
// that makes branded types ergonomic — use typed IDs
type OrderId = Brand<string, 'OrderId'>
type CustomerId = Brand<string, 'CustomerId'>
Examples
Nominal type system — typed IDs prevent accidental swaps at compile time:
fun assignOrderToCustomer(orderId: OrderId, customerId: CustomerId) { /* ... */ }
val orderId = OrderId(UUID.randomUUID())
val customerId = CustomerId(UUID.randomUUID())
assignOrderToCustomer(orderId, customerId) // compiles
assignOrderToCustomer(customerId, orderId) // compile error
Structural type system without low-friction mechanism — rely on naming:
function assignOrderToCustomer(orderId: string, customerId: string) { /* ... */ }
// The compiler does not catch this swap — naming discipline is the safeguard
assignOrderToCustomer(orderId, customerId)
Review Questions
When reading or reviewing code, ask:
- Is the project's type system nominal or structural?
- If nominal, are domain entity IDs defined as distinct typed IDs?
- If structural, does the project use a low-friction mechanism for nominal distinction, or does it use the underlying type directly?
- Is the approach consistent across all domain entity identifiers in the project?
- If typed IDs are used in a structural type system, do they add significant boilerplate or friction?
If the approach does not match the type system and project conventions, apply this skill.
Report the Outcome
When finishing the task:
- state the project's type system classification — nominal or structural
- state which domain entity ID types were created or changed
- state whether typed IDs or the underlying type was used, and why
- state whether the approach is consistent with the rest of the project
1---2name: domain-entity-typed-ids3description: Determine how to type domain entity identifiers based on the project's type system. Use when an agent needs to create, modify, review, or interpret the type of a domain entity's identifier. In languages with a nominal type system, use typed IDs — a distinct type per domain entity that wraps the underlying identifier type. In languages with a structural type system, use typed IDs only if the language offers a low-friction mechanism to make them nominally distinct. Otherwise, use the underlying identifier type directly.4---56# Domain Entity Typed IDs78## Goal910Choose the right level of type distinction for domain entity identifiers based on whether the project's type system is nominal or structural.1112In a nominal type system, two types with the same structure are distinct if they have different names. A typed ID — a dedicated type per domain entity wrapping the underlying identifier — provides compile-time safety against mixing identifiers of different entities at no meaningful cost.1314In a structural type system, two types with the same structure are interchangeable regardless of their names. A typed ID only provides safety if the language offers a low-friction mechanism to make the types nominally distinct. If achieving nominal distinction requires patterns that add significant boilerplate, ceremony, or ergonomic friction, use the underlying identifier type directly instead.1516## What Counts as In Scope1718Apply this skill to code that does one or more of these things:1920- defines the type of a domain entity's identifier21- introduces a new domain entity that needs an identifier type22- changes or refactors how a domain entity's identifier is typed23- reviews whether an identifier type provides appropriate type safety for the project's type system2425## The Rule26271. In nominal type systems, use typed IDs.28 - Define a distinct type per domain entity that wraps the underlying identifier type.29 - Use the language's idiomatic lightweight wrapper — inline classes, value classes, newtypes, or the equivalent.30 - The typed ID ensures that identifiers of different entities are incompatible at compile time.31322. In structural type systems, assess friction before choosing.33 - If the language offers a low-friction mechanism to make structurally identical types nominally distinct — and that mechanism does not add significant boilerplate to construction, serialization, or persistence — use typed IDs.34 - If achieving nominal distinction requires patterns that are verbose, non-idiomatic, or poorly supported by the ecosystem — such as branded types that need manual casting, custom constructors, and special serialization handling — use the underlying identifier type directly.35363. When using the underlying type directly, rely on parameter naming for clarity.37 - Name parameters and fields clearly — `customerId`, `orderId` — so the intent is readable even without type distinction.38 - Accept that the compiler will not catch accidental ID swaps in this case.39404. Do not mix approaches within the same project.41 - All domain entity identifiers in a project must follow the same convention — either all typed IDs or all underlying type.42 - Follow the project's established convention. If no convention exists, choose based on the rules above and apply consistently.4344## Detection Workflow45461. Determine the type system of the project's language.47 - Nominal: Kotlin, Java, Scala, Rust, Swift, C#, Go, Haskell — types are distinct by name.48 - Structural: TypeScript, Python, Elixir, Clojure — types are interchangeable if structurally identical.49502. Check the project's existing convention.51 - Look for how existing domain entity IDs are typed.52 - If a convention exists, follow it.53543. If no convention exists, apply the rule.55 - Nominal type system → use typed IDs.56 - Structural type system → assess whether a low-friction mechanism exists for nominal distinction. If yes, use typed IDs. If no, use the underlying type directly.5758## Writing or Changing Domain Entity ID Types59601. For nominal type systems — define a typed ID per domain entity:6162 ```kt63 // Kotlin — inline value class64 @JvmInline65 value class OrderId(val value: UUID)6667 @JvmInline68 value class CustomerId(val value: UUID)69 ```7071 ```java72 // Java — record73 public record OrderId(UUID value) {}74 public record CustomerId(UUID value) {}75 ```7677 ```rs78 // Rust — newtype79 pub struct OrderId(pub Uuid);80 pub struct CustomerId(pub Uuid);81 ```8283 ```swift84 // Swift — struct wrapper85 struct OrderId: Hashable {86 let value: UUID87 }8889 struct CustomerId: Hashable {90 let value: UUID91 }92 ```9394 ```cs95 // C# — readonly record struct96 public readonly record struct OrderId(Guid Value);97 public readonly record struct CustomerId(Guid Value);98 ```99100 ```go101 // Go — named type102 type OrderId uuid.UUID103 type CustomerId uuid.UUID104 ```1051062. For structural type systems where typed IDs add friction — use the underlying type:107108 ```ts109 // TypeScript — use the underlying type directly110 class Order {111 readonly id: string112 readonly customerId: string113 }114 ```115116 ```py117 // Python — use the underlying type directly118 @dataclass(frozen=True)119 class Order:120 id: UUID121 customer_id: UUID122 ```1231243. For structural type systems where a low-friction mechanism exists — use typed IDs:125126 ```ts127 // TypeScript with a library like ts-brand or a project convention128 // that makes branded types ergonomic — use typed IDs129 type OrderId = Brand<string, 'OrderId'>130 type CustomerId = Brand<string, 'CustomerId'>131 ```132133## Examples134135Nominal type system — typed IDs prevent accidental swaps at compile time:136137```kt138fun assignOrderToCustomer(orderId: OrderId, customerId: CustomerId) { /* ... */ }139140val orderId = OrderId(UUID.randomUUID())141val customerId = CustomerId(UUID.randomUUID())142143assignOrderToCustomer(orderId, customerId) // compiles144assignOrderToCustomer(customerId, orderId) // compile error145```146147Structural type system without low-friction mechanism — rely on naming:148149```ts150function assignOrderToCustomer(orderId: string, customerId: string) { /* ... */ }151152// The compiler does not catch this swap — naming discipline is the safeguard153assignOrderToCustomer(orderId, customerId)154```155156## Review Questions157158When reading or reviewing code, ask:159160- Is the project's type system nominal or structural?161- If nominal, are domain entity IDs defined as distinct typed IDs?162- If structural, does the project use a low-friction mechanism for nominal distinction, or does it use the underlying type directly?163- Is the approach consistent across all domain entity identifiers in the project?164- If typed IDs are used in a structural type system, do they add significant boilerplate or friction?165166If the approach does not match the type system and project conventions, apply this skill.167168## Report the Outcome169170When finishing the task:171172- state the project's type system classification — nominal or structural173- state which domain entity ID types were created or changed174- state whether typed IDs or the underlying type was used, and why175- state whether the approach is consistent with the rest of the project