DPC Zettelkasten Open in the explorer

Value Class Identifier

Every record's id is a Kotlin inline value class wrapping a UUID string, making identifiers type-safe at compile time and free at runtime.

MfFactionId, MfPlayerId, MfLawId, MfGateId, MfLockedBlockId, MfDuelId, MfFactionRoleId, MfApprovalRequestId, MfFactionRelationshipId — all nine identifiers in the plugin are @JvmInline value class wrappers around a String, with no exceptions.

What it buys

At compile time these are distinct types: a function taking an MfFactionId cannot be handed an MfPlayerId, even though both are strings underneath. In a codebase where nearly every method signature takes at least one identifier, that is the difference between a compiler error and a bug that only appears when two UUIDs get swapped.

At runtime, Kotlin erases the wrapper. An MfFactionId in a local variable is a String — no allocation, no indirection. The safety is free.

Where the boundary lives

Each id class owns its own conversions, which keeps the awkward casts in one place:

That second pair is the more interesting one: it means the domain model refers to players by a value it owns, not by a Bukkit object whose lifecycle the server controls.

The catch

Value classes and Java interoperation do not always agree, which is why domain classes are peppered with @get:JvmName("getId"). Without it, Kotlin mangles the getter name for a value-class-typed property, and Java callers — including expansions written in Java such as Fiefs — cannot see it. The annotation is the tax for keeping the public API usable from Java.

The unwrapping to a plain column value happens in the Repository Pattern implementations, so the database sees only strings.

Sources

Linked from