Parent-Child Persistence
The pattern of an entity (child) existing in the context of another entity (parent) is common in enterprise systems: Documents with their lines, Items with Units of Measure, Companies with Roles they play.
The lifecycle and mutations of Child entities are always done through the Parent Entity Service.
Design Principles
Section titled “Design Principles”Children are scoped to their parent through three complementary mechanisms:
- A metadata column holding the parent identifier (typically the parent
eId) - A UniversalCondition that automatically applies the parent filter to all operations
- A Validator that rejects operations when the provided metadata parent does not match the Universe’s parent
Decision: Ordered vs Unordered
Section titled “Decision: Ordered vs Unordered”Use ordered (ranked) parent-child when:
- The child collection must be displayed in a stable order
- Users can insert at a position and/or reorder items
- You need ordering semantics independent of timestamps
Use unordered parent-child when:
- The child collection has no ordering requirements
- You only need “all children for this parent” semantics
- You want the simplest parent-child scoping implementation
Ordered (Ranked) Parent-Child
Section titled “Ordered (Ranked) Parent-Child”Structure
Section titled “Structure”- The child table includes:
parent_eid,line_rank - The child metadata contains:
parentEid,rank - The child Universe is parametrized by
parentEidand implements ordered operations: add/insert, reorder, list ordered by rank
Implementation
Section titled “Implementation”// Child table columnsabstract class ChildTable(cfg: TableConfiguration): UniverseTable(cfg) { val parentEid = uuid("parent_eid") val rank = long("line_rank")}
// Child metadatadata class OrderLineMetadata( override val parentEid: UUID, override val rank: Long,): LineMetadataUniverse constraint: Filter all operations by parent_eid == parentEid, list operations order by rank ascending, maintain rank gaps to support insertion without rebalancing.
Example: ../operations/src/main/kotlin/cards/arda/operations/procurement/orders/persistence/OrderLineUniverse.kt
Unordered Parent-Child
Section titled “Unordered Parent-Child”Structure
Section titled “Structure”- The child table includes: a parent reference column (e.g.,
item_eid) - The child metadata contains: the parent
eId(e.g.,itemEid) - The child Universe is parametrized by the parent
eIdand applies a Universe-level constraint
Implementation
Section titled “Implementation”// Metadata includes parent referencedata class ItemSupplyMetadata( override val tenantId: UUID, val itemEid: UUID,) : ScopedMetadata { companion object { const val COLUMN_ITEM_EID = "item_eid" }}
// Table maps metadata column + payloadobject ITEM_SUPPLY_TABLE : ScopedTable(ItemSupplyTableConfiguration) { val itemEid = uuid(ItemSupplyMetadata.COLUMN_ITEM_EID) val supplier = varchar("supplier", 255) // ...}
// Universe-level constraintprivate class ItemSupplyUniversalCondition(private val itemEid: UUID) : ScopedUniversalCondition() { override fun filter(ctx: ApplicationContext): Result<Filter> = super.filter(ctx).map { tenantFilter -> Filter.And(listOf( tenantFilter, Filter.Eq(ItemSupplyMetadata.COLUMN_ITEM_EID, itemEid) )) }}
// Validator rejects mismatched parentprivate fun checkParent(metadata: ItemSupplyMetadata): Result<Unit> = when (metadata.itemEid) { u.itemEid -> Result.success(Unit) else -> Result.failure(AppError.ArgumentValidation("item_eid", "Parent mismatch")) }Example: ../operations/src/main/kotlin/cards/arda/operations/reference/item/persistence/ItemSupplyUniverse.kt
Guidelines for New Parent-Child Entities
Section titled “Guidelines for New Parent-Child Entities”-
Decide ordered vs unordered up front:
- If you need “insert at index” or “reorder”: use the ordered pattern with
rank - Otherwise: use the unordered pattern (simpler and cheaper)
- If you need “insert at index” or “reorder”: use the ordered pattern with
-
Always scope children in two places:
UniversalCondition(enforced on every query/mutation)Validator(enforced on metadata consistency)
-
Add the right indexes:
- Always index
eid,tenant_id,effective_as_of,recorded_as_of - Always index the parent column (e.g.,
parent_eid/item_eid) - If ordered: consider
(parent_eid, line_rank)for list performance
- Always index
-
Keep parent references in metadata, not payload:
- Makes scoping a structural concern rather than business data
-
Consider uniqueness constraints:
- For unordered children where you need “one supply per supplier per item”, add a unique index like
(tenant_id, item_eid, supplier)(with optional partial index forretired=false)
- For unordered children where you need “one supply per supplier per item”, add a unique index like
Structured Locator Translators for Child Universes
Section titled “Structured Locator Translators for Child Universes”Child universes should define an EntityServiceConfiguration and bind it to the ChildTable to enable JSON field-name locators. Due to invariant generics on ExposedLocatorTranslator, the persistence.bt reference must be cast to ChildTable with a runtime guard:
val childQueryConfig = EntityServiceConfiguration.create(ChildPayload::class).also { it.freeze() }
// Module-level lazy QueryCompiler (reuse in validators and services)internal val childQCompiler by lazy { QueryCompiler(CHILD_TABLE, childQueryConfig.bindToTable(CHILD_TABLE))}
class ChildUniverse(...) : SimpleChildUniverse.Impl<...>(...) { override val translator by lazy { check(persistence.bt is ChildTable) { "ChildUniverse requires a ChildTable, got ${persistence.bt::class}" } childQueryConfig.bindToTable(persistence.bt as ChildTable) }}See Query DSL: Child Universe Translator Pattern.
Cascade Delete
Section titled “Cascade Delete”When deleting child entities within a parent’s delete operation, using ChildUniverse.delete() can fail due to parent lookup timing mismatches within the same transaction. Use low-level recordCompanion.delete() to bypass parent lookup.
Note: ChildUniverse.delete returns Pair<BitemporalEntity<PP, PM>, BitemporalEntity<EP, EM>> — parent and child entities, not just the child.
See Also
Section titled “See Also”- Information Model Design — design-time playbook for defining new entities (including parent-child) with the right shape, naming, and reference patterns.
- Universe Design —
ChildMetadataand the parent-scopedUniversetypes. - Bitemporal Persistence — the bitemporal column conventions that apply equally to child entities.
Copyright: © Arda Systems 2025-2026, All rights reserved