Universe Design
The Universe framework provides a structured way to manage bitemporal entities, with support for global and scoped (e.g., tenant-based) data segregation.
Class Hierarchy
Section titled “Class Hierarchy”Universe<EP, M> (interface)└── AbstractUniverse<EP, M, TBL, PR> (abstract) └── AbstractScopedUniverse<EP, M, TBL, PR> (abstract)
Validator<EP, M> (interface)└── ScopingValidator<EP, M>
UniversalCondition (interface)└── ScopedUniversalCondition
UniverseTable (abstract)└── ScopedTable
BitemporalRecord<EP, M, TBL, SELF> (abstract)└── ScopedRecord<EP, M, TBL, SELF>
Persistence<EP, M, TBL, PR> (abstract)Universe Interface
Section titled “Universe Interface”Universe<EP, M> defines the contract for a collection of bitemporal entities:
EP: Entity payload type, must implementEntityPayloadM: Payload metadata type, must implementPayloadMetadata
Operations:
create(payload, metadata, asOf, author): Creates a new entityread(eId, asOf, includeRetired): Reads as-of a specific timereadRecord(rId): Reads a specific historical record by record IDfindOne(filter, asOf, includeDeleted): Finds a single entity matching a filterlist(query, asOf, includeRetired, withTotal): Lists with filtering/sorting/paginationcount(filter, asOf, includeRetired): Counts matching entitiesupdate(update): Creates a new version of an existing entitydelete(originEId, metadata, asOf, author): Logical delete (creates retired record)aggregate(query, asOf, aggregation, includeRetired, mapper): Aggregation operationshistory(eId, from, to, page): Historical records within a time range
All operations return DBIO<T> and are suspend functions.
AbstractUniverse
Section titled “AbstractUniverse”Provides the skeletal implementation of Universe. Dependencies:
persistence: Persistence<EP, M, TBL, PR>: Database interactionvalidator: Validator<EP, M>: Payload and operation validationuniversalCondition: UniversalCondition: Global filters (scoping)
The universalCondition is applied to all read, list, count, update, and delete operations. The validator is invoked before create, update, and delete operations.
universalConditionmust beprotectedvisibility. Overriding withprivatetriggers Kotlin compilation errors.
AbstractScopedUniverse
Section titled “AbstractScopedUniverse”Extends AbstractUniverse for tenant-scoped data. Mandates:
ScopedMetadata(includestenantId)ScopedTable(includestenantIdcolumn)ScopedRecord(managestenantIdmapping)ScopingValidator(validatestenantIdagainst context)ScopedUniversalCondition(filters bytenantIdfrom context)
Validation Layering
Section titled “Validation Layering”Two tiers of validation:
1. Payload-Level Validation (EntityPayload.validate)
Section titled “1. Payload-Level Validation (EntityPayload.validate)”On the entity payload itself. Validates:
- Format checks (email, URL)
- Range checks
- Required field checks
- Internal consistency between payload fields
override fun validate(ctx: ApplicationContext, mutation: Mutation): Result<Unit> { if (name.isBlank()) return Result.failure(AppError.ArgumentValidation("name", "Name cannot be blank")) return Result.success(Unit)}2. Universe-Level Validation (Validator<EP, M>)
Section titled “2. Universe-Level Validation (Validator<EP, M>)”Handles checks requiring broader context:
- Cross-entity validation (uniqueness, referential integrity)
- State transition validation
- Authorization-related checks
- Tenant/parent scoping verification
Methods:
validateForCreate(ctx, payload, metadata, asOf, author): DBIO<Unit>validateForUpdate(ctx, payload, metadata, asOf, author, idempotency, previous): DBIO<Unit>validateForDelete(ctx, candidate, metadata, asOf, author): DBIO<Unit>
Step-by-Step: Building a New Scoped Universe
Section titled “Step-by-Step: Building a New Scoped Universe”Step 1: Define Entity Payload
Section titled “Step 1: Define Entity Payload”@Serializabledata class YourEntityPayload( @Serializable(with = UUIDSerializer::class) override val eId: EntityId, val name: String,) : EntityPayload { override fun validate(ctx: ApplicationContext, mutation: Mutation): Result<Unit> { if (name.isBlank()) return Result.failure(AppError.ArgumentValidation("name", "Name cannot be blank")) return Result.success(Unit) }}Step 2: Define Scoped Metadata
Section titled “Step 2: Define Scoped Metadata”@Serializabledata class YourScopedMetadata( @Serializable(with = UUIDSerializer::class) override val tenantId: UUID) : ScopedMetadataStep 3: Define Scoped Table
Section titled “Step 3: Define Scoped Table”object YourScopedTable : ScopedTable(TableConfiguration("YOUR_ENTITY_TABLE_NAME")) { val name = varchar("item_name", 255) // Bitemporal and scoped columns (eId, rId, tenantId, effectiveAsOf, etc.) are inherited}Step 4: Define Scoped Record
Section titled “Step 4: Define Scoped Record”class YourScopedRecord(rId: EntityID<UUID>) : ScopedRecord<YourEntityPayload, YourScopedMetadata, YourScopedTable, YourScopedRecord>(rId, YourScopedTable) {
var name by YourScopedTable.name
override fun fillPayload(p: YourEntityPayload) { payload = p this.name = p.name }
override fun fillMetadata(m: YourScopedMetadata) { metadata = m this.tenantId = m.tenantId }
companion object : Persistence<YourEntityPayload, YourScopedMetadata, YourScopedTable, YourScopedRecord>( YourScopedTable, YourScopedRecord::class.java, ::YourScopedRecord )}Step 5: Define Scoping Validator
Section titled “Step 5: Define Scoping Validator”fun validatorFor(universe: YourUniverse): ScopingValidator<YourEntityPayload, YourScopedMetadata> { return object : ScopingValidator<YourEntityPayload, YourScopedMetadata>() { override suspend fun validateForCreate( ctx: ApplicationContext, payload: YourEntityPayload, metadata: YourScopedMetadata, asOf: TimeCoordinates, author: String ): DBIO<Unit> = suspend { super.validateForCreate(ctx, payload, metadata, asOf, author)().flatMap { if (payload.name.isBlank()) Result.failure(AppError.ArgumentValidation("name", "Entity name cannot be blank")) else Result.success(Unit) } } }}Step 6: Define Universal Condition
Section titled “Step 6: Define Universal Condition”object YourUniversalCondition : ScopedUniversalCondition()Step 7: Implement the Universe
Section titled “Step 7: Implement the Universe”// Define EntityServiceConfiguration for structured locator resolutionval yourQueryConfig = EntityServiceConfiguration.create(YourEntityPayload::class) { opaque("settings") // exclude non-filterable fields}.also { it.freeze() }
class YourUniverse : AbstractScopedUniverse< YourEntityPayload, YourScopedMetadata, YourScopedTable, YourScopedRecord>(), LogEnabled by LogProvider(YourUniverse::class) {
override val persistence = YourScopedRecord.Companion override val validator = validatorFor(this) override val universalCondition = YourUniversalCondition override val translator by lazy { yourQueryConfig.bindToTable(persistence.bt) }
// Custom business methods suspend fun customMethod(filter: Filter, asOf: TimeCoordinates): DBIO<List<YourBusinessObject>> { return aggregate(Query(filter), asOf, Aggregation(...), includeRetired = false) { row -> // row mapping } }}The translator enables API clients to use JSON field names (camelCase) as query locators, while remaining backward-compatible with raw column names. See Query DSL: EntityServiceConfiguration for details.
Step 8: Testing
Section titled “Step 8: Testing”Use AbstractUniverseTestTemplate for comprehensive CRUD/query/history coverage:
class YourUniverseTest : AbstractUniverseTestTemplate< YourEntityPayload, YourScopedMetadata, YourScopedTable, YourScopedRecord, YourUniverse>( testName = "YourUniverse", appConfigPath = "test-application.conf", newUniverse = { YourUniverse() }, serviceScope = { ServiceScope.Tenant(testTenantId) }, newPayload = { eId, testCtx, order -> YourEntityPayload(eId, "test-$testCtx-$order") }, newMetadata = { _, tenantIdStr, _ -> YourScopedMetadata(UUID.fromString(tenantIdStr)) }) { // Custom tests here}Real-World Example: KanbanCardUniverse
Section titled “Real-World Example: KanbanCardUniverse”The KanbanCardUniverse extends AbstractScopedUniverse with a custom aggregation method for status summaries:
class KanbanCardUniverse : AbstractScopedUniverse<KanbanCard, KanbanCardMetadata, KANBAN_CARD_TABLE, KanbanCardRecord>(), LogEnabled by LogProvider(KanbanCardUniverse::class) {
override val persistence = KanbanCardRecord.Companion override val universalCondition = KanbanCardUniversalCondition override val validator = validatorFor(this)
suspend fun summaryByStatus( condition: Filter, asOf: TimeCoordinates, includeRetired: Boolean, vararg forStatus: KanbanCardStatus, ): DBIO<List<KanbanCardSummary>> = aggregate( Query( Filter.And(listOf(condition, Filter.In(persistence.bt.status.name, forStatus.toList()))), Sort(listOf(SortEntry(persistence.bt.status.name, SortDirection.ASC))), paginate = Pagination(0, 500) ), asOf, Aggregation( listOf(GroupBy.Classifier(persistence.bt.status.name)), listOf(GroupBy.Aggregator(persistence.bt.cardQuantity.amount.name, AggregationType.SUM, "quantityAmount")) ), includeRetired = includeRetired, rowMapper(asOf) ).map { /* post-processing */ }}AbstractUniverse vs AbstractScopedUniverse
Section titled “AbstractUniverse vs AbstractScopedUniverse”| Feature | AbstractUniverse | AbstractScopedUniverse |
|---|---|---|
| Scope | Generic (global or custom) | Tenant-based |
| Metadata | PayloadMetadata | ScopedMetadata (with tenantId) |
| Table | UniverseTable | ScopedTable (with tenantId column) |
| Record | BitemporalRecord | ScopedRecord (manages tenantId) |
| Validator | Validator<EP, M> | ScopingValidator<EP, M> |
| Universal Condition | UniversalCondition | ScopedUniversalCondition |
| Use Case | Foundation for any universe | Entities partitioned by tenant |
See Also
Section titled “See Also”- Information Model Design — design-time playbook for defining new entities, value objects, references, and persistence with the right shape and naming.
- Bitemporal Persistence — bitemporal time coordinates and table schema.
- Parent-Child Persistence — parent-child entity scoping and ordering.
- Data Authority Module Pattern § Cross-Service Isolation — the binding rule for when two services share a component but not their universes.
Copyright: © Arda Systems 2025-2026, All rights reserved