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.
Convention
Section titled “Convention”| Composite? | Use | Examples |
|---|---|---|
| Single-typed (one underlying primitive or built-in type) | typealias | typealias TenantSlug = Stringtypealias EncryptedToken = Stringtypealias PostmarkServerId = Long |
| Multi-field | @Serializable data class | Money(value, currency)Quantity(amount, unit)Attachment(name, contentType, content) |
Why typealiases for single-typed values
Section titled “Why typealiases for single-typed values”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 thanString, but every callsite has to either box or.valueto 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.
Why data classes for composites
Section titled “Why data classes for composites”Composite value objects encode genuinely-multi-field structure:
@Serializabledata class Money(val value: BigDecimal, val currency: CurrencyCode)
@Serializabledata class Quantity(val amount: BigDecimal, val unit: UnitOfMeasure)
@Serializabledata 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):
@Serializablesealed interface Identifier { @Serializable data class External(val system: String, val value: String) : Identifier @Serializable data class Internal(val uuid: UUID) : Identifier}Persistence
Section titled “Persistence”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.
See Also
Section titled “See Also”- Identity —
EntityId,Binding, identity-class types. - Entity References — references as a special case of composite value object.
- Information Model Design — design-time playbook.
- Bitemporal Persistence — how value objects appear in bitemporal table schemas.
Copyright: © Arda Systems 2025-2026, All rights reserved