Skip to content

Value Objects

A value object is a small, immutable type that represents a meaningful concept in the domain (a slug, an encrypted token, an amount of money) without having its own identity or lifecycle. Value objects are part of an entity’s payload, addressing, or reference shape; they are not entities themselves.

This page records the convention for declaring value objects in Kotlin within Arda backend modules.

Composite?UseExamples
Single-typed (one underlying primitive or built-in type)typealiastypealias TenantSlug = String
typealias EncryptedToken = String
typealias PostmarkServerId = Long
Multi-field@Serializable data classMoney(value, currency)
Quantity(amount, unit)
Attachment(name, contentType, content)

Avoid wrapping a single primitive in a data class “for type safety”:

  • The Kotlin compiler treats data class TenantSlug(val value: String) as a different type than String, but every callsite has to either box or .value to get the underlying string — the boilerplate dwarfs the win.
  • Inline value classes (@JvmInline value class TenantSlug(val value: String)) avoid boxing but still impose the wrapping at every callsite.
  • Type aliases give readable signatures (fun resolveSlug(input: TenantSlug): TenantSlug) without forcing wrapper allocation or accessor noise.

Reserve genuinely-typed value classes for cases where the primitive’s meaning must be enforced even within the module (e.g., when there’s a real risk of mixing two String-shaped values that must not be confused). Most of the time, a typealias plus naming discipline at the field level is enough.

Composite value objects encode genuinely-multi-field structure:

@Serializable
data class Money(val value: BigDecimal, val currency: CurrencyCode)
@Serializable
data class Quantity(val amount: BigDecimal, val unit: UnitOfMeasure)
@Serializable
data class Attachment(val name: String, val contentType: String, val content: String)

The data class’s auto-generated equals / hashCode / copy / componentN are useful in domain code; serialization is straightforward via kotlinx.serialization.

When a composite has multiple persisted variants, use a sealed interface with named nested data classes (the same pattern as entity references):

@Serializable
sealed interface Identifier {
@Serializable data class External(val system: String, val value: String) : Identifier
@Serializable data class Internal(val uuid: UUID) : Identifier
}

Composite value objects often appear inside entity payloads. They are flattened into the persisted table with prefix-based column naming — the persistence layer (<Entity>Persistence.kt / <Entity>Record) handles the round-trip between flattened columns and the in-memory composite.

Two prefix styles in current use:

-- snake_case prefix (preferred for new tables):
primary_supply_unit_cost_value DOUBLE PRECISION,
primary_supply_unit_cost_currency VARCHAR(5),
-- mixed-case quoted (legacy / strongly-grouped):
"PRIMARY_SUPPLY_supplier" VARCHAR(255),
"ITEM_REFERENCE_entity_id" uuid,

For the design-time playbook covering how value objects fit into a new entity’s design, see Information Model Design.