Skip to content

Data Authority Module Pattern

A Data Authority Module is the primary pattern for implementing modules that own and manage a set of entities. It has a well-defined four-layer structure and a standard pattern for exposing CRUDQ operations.

Module Structure

A Data Authority Module consists of four distinct layers:

Responsible for:

  • Handling communication protocols that expose the module’s API Endpoints
  • Protocol data marshalling and unmarshalling
  • Entity identity extraction from requests
  • Routing requests to the appropriate operations in the Service Layer
  • Conversion between wire timing/concurrency models and internal Service concurrency model (synchronous vs. asynchronous, multipart messages)

Responsible for:

  • Definition of data structure, validation, and behavior of individual entities and value objects managed by the service
  • All business logic implementable by traversing the object graph reachable from an entity instance
  • Transactional boundary of the module — individual operations typically represent transactional updates
  • Coordination of persistence operations across multiple Universes (e.g., referential integrity between separate entities)
  • Coordination of proxy operations that invoke other modules’ services, including distributed commit/rollback (saga patterns)
  • Generation of entity identities and ADNs; interpretation of ADNs to resolve entity values
  • Generation and emitting of Notifications in response to changes

Responsible for:

  • Storage and retrieval of entity values from persistent storage
  • Management of bitemporal changes to entity values
  • Validation and management of entity collections (concept of Universe) including uniqueness and ordering
  • Defining EntityServiceConfiguration per entity type to build structured locator translators that map JSON field names to database columns for the Query DSL

Responsible for:

  • Providing access to API Endpoints of other modules
  • Allowing the Service Layer to interact with other modules without knowing their implementation or runtime location

API Endpoints expose module capabilities to other modules and external systems. Each module may expose multiple API Endpoints.

Specify a set of operations, each defined by a Pair<Request, Response>. Operations always specify a response (which may be a simple acknowledgement). May be synchronous or asynchronous.

PlantUML diagram

Implemented using:

  • HTTP/REST for Request/Response
  • gRPC for Request/Response
  • SQS, WebSockets, Kinesis, or Kafka for Streaming

Produce or consume a stream of Notifications. Usually asynchronous.

LayerKey Responsibilities
Protocol AdaptorWire serialization/deserialization, protocol-specific concerns, request routing
ServiceBusiness logic, transaction boundaries, multi-universe coordination, notifications
PersistenceBitemporal storage, collection management (Universe), scoping
ProxyAbstracted access to other modules’ endpoints

Arda’s Ktor-based implementation follows this wiring pattern (see Ktor Module Wiring):

fun Application.myModule(
inComponent: ComponentConfiguration,
locator: EndpointLocator.Rest,
cfg: ModuleConfig,
authentication: Authentication,
injectedUniverse: MyUniverse? = null,
injectedService: MyService? = null,
): MyService {
val db: Database = DataSource(cfg.dataSource!!.db, cfg.dataSource!!.pool)
.newDb(cfg.dataSource!!.flywayConfig)
val service = injectedService ?: MyService.Impl(injectedUniverse ?: MyUniverse(), db)
val endpoint = MyEndpoint.Impl(cfg, locator, service)
MultiEndpointKtorModule(inComponent, cfg, authentication, listOf(endpoint))
.configureServer(this)
return service
}

See examples in the item-data-authority repository.

The Universe concept represents the complete collection of entities managed by the persistence layer for a module. Its design is covered in Universe Design.

The universe-definition.md source document is marked “To be added” — content is available in the universe-design.md page.

Two services co-located in the same component must not share persistence. The boundary between services is enforced at four levels:

  1. Each service owns its own Universe instance. No service reads from or writes to another service’s universe. The shared Database connection pool is reused, but the universes are independent.
  2. Each service interface method establishes its own transaction via inTransaction(db) { ... }. A cross-service interface call crosses the transaction boundary — the calling service’s transaction does not extend across the call; the called service starts its own.
  3. No SQL FOREIGN KEY constraints across service boundaries. Cross-service references are stored as soft references in the holder’s table — either as a UUID-only column or as an Arda Domain Name (ADN) string when the holder needs to address an entity in another universe without depending on the foreign service’s schema. Within a single universe (e.g., the bitemporal previous chain, or a parent-child child-to-parent reference), foreign keys are fine and encouraged.
  4. Resolution at runtime goes through the owning service’s interface. The calling service does not query the other service’s tables or universe directly; it calls the interface method (itemService.getAsOf(itemEId, asOf), etc.), which returns through that service’s transaction.

Concrete examples in the operations repo:

  • OrderLine.itemEId: UUID references Item with no FK; resolution via itemService.getAsOf(...).
  • KanbanCard.ITEM_REFERENCE_* references Item (flattened reference value object) with no FK; resolution via itemService.getAsOf(...).
  • The V001__order.sql migration explicitly omits the order_line.item_eid FK to the item table.

This isolation supports independent service evolution, single-service-scoped transactional invariants, and clean operational boundaries (e.g., one service’s deployment cannot break another’s writes mid-transaction).

Rule: Coordinate Through the Owning Service, Not Its Universe

Section titled “Rule: Coordinate Through the Owning Service, Not Its Universe”

A service must not reach into another module’s universe or persistence to resolve a related entity — even when that universe is conveniently in hand. Reading the parent via a child universe’s parent-universe handle (e.g. supplyUniverse.parentUniverse.read(...)) is wrong: it bypasses the owning service’s transaction and validation, and couples the caller to the foreign schema.

Coordinate through the owning service interface instead. ItemSupplyCrudService reads the parent Item via parentService.getAsOf(parentEId, asOf), not through the supply universe’s parent universe:

// Read the parent through its owning service — the CRUD service coordinates with the
// Item service, it does not reach into the supply universe's parent universe.
parentService.getAsOf(parentEId, asOf).flatMap { parentRecord ->
// ... resolve, validate, persist within the service's own transaction
}

Note: Intra-Module Service Dependency Cycles — Two-Phase Wiring

Section titled “Note: Intra-Module Service Dependency Cycles — Two-Phase Wiring”

The rule above pushes coordination through service interfaces, which can create a genuine construction-time cycle between two services in the same module (service A needs B, and B needs A). The accepted resolution is two-phase wiring, not field-injection hacks or lazy back-references:

  1. Construct both services.
  2. Immediately wire the back-reference via an explicit setParentService / wire... call invoked in the owning service’s init block, right after construction and before any operation runs.

ItemService.Impl uses this shape to wire itself into its printing sub-service (ItemPrintingService.Impl) in its init block. Use two-phase wiring only for genuinely irreducible cycles co-located in one module; prefer eliminating the cycle altogether. The Item↔ItemSupply pair originally used a setParentService back-reference here, but 6.0.0 removed it — the supply reaction now returns its updated rows for the parent ItemService to project (via ItemVendorCascade), leaving Item → ItemSupply one-directional. Treat that as the preferred outcome and reach for two-phase wiring only when the back-reference is truly unavoidable. See the Item Module.

For the soft-reference value-object pattern in Kotlin, see Entity References § Implementation in Kotlin. For the design playbook covering when and how to apply these rules to a new entity, see Information Model Design.

  • Information Model Design — design-time playbook for defining new entities with the right shape, naming, and reference patterns.
  • Module Concept — the general module abstraction (state encapsulation, endpoints, bindings); the Data Authority Module is its entity-owning specialization.
  • Data Authority Limitations — known constraints and edge cases of the Data Authority query system.