Skip to content

Architecture Explorations

Exploratory documents capturing architectural patterns, comparisons, and conventions used across the Arda backend. These are design references, not active project specifications.

The Arda codebase deliberately diverges from standard Spring Boot / JPA / Hibernate patterns. The key contrasts:

Persistence — Bitemporality vs. Mutable State

Section titled “Persistence — Bitemporality vs. Mutable State”

Standard systems overwrite database rows and use separate audit logs. Arda uses an append-only bitemporal model with TimeCoordinates (Valid Time + Transaction Time):

  • Advantage: Complete audit trail. Time-travel queries (asOf) to reconstruct past state.
  • Trade-off: Every read/write requires time coordinates. Joining tables is significantly harder.

Standard systems use Hibernate with managed entities and lazy loading. Arda uses explicit Universe objects wrapping Exposed (a Kotlin SQL DSL):

  • Advantage: All database IO is explicit. No N+1 surprises. Immutable EntityPayload data classes eliminate mutation bugs.
  • Trade-off: More boilerplate: Table, Record, Persistence, and Universe definitions per entity.

Business Logic — State Engine vs. Procedural Service

Section titled “Business Logic — State Engine vs. Procedural Service”

Standard systems use service methods (submitOrder, approveOrder) that mutate state fields inline. Arda uses a declarative state machine: states, signals, and transitions defined as objects:

  • Advantage: Lifecycle is a distinct, visualizable artifact. Guard blocks centrally enforce pre-conditions. Impossible to execute illegal transitions.
  • Trade-off: Significant setup for simple CRUD; following the flow requires reading multiple definitions.

Error Handling — Result Monad vs. Exceptions

Section titled “Error Handling — Result Monad vs. Exceptions”

Standard systems throw checked exceptions and catch at the controller level. Arda returns Result<T> from all operations:

  • Advantage: Function signatures are honest about failure modes. Type safety forces error handling. Errors compose naturally in pipelines.
  • Trade-off: flatMap chains can appear daunting to OO developers.

Dependency Injection — Manual Module Wiring vs. Spring IoC

Section titled “Dependency Injection — Manual Module Wiring vs. Spring IoC”

Standard systems use @Autowired with classpath scanning. Arda uses explicit Module.kt files with constructor wiring:

  • Advantage: Zero magic. Missing dependencies are compile errors, not runtime exceptions. Unit tests instantiate services in microseconds.
  • Trade-off: Adding a dependency deep in the graph requires passing it down the constructor chain.

All domain entities implement EntityPayload. Use Kotlin data classes with @optics for deep immutable updates and Arrow for optics. Use value objects (Money.Value, DateTime) instead of primitives where the domain concept is reusable.

Always interact with the database through a Universe instance. Never call Exposed DAO directly from a service. TimeCoordinates.now() must be called at the top level and passed in — never inside a loop.

Use ChildUniverse for entities with child records (e.g., Order to OrderLines). Updates to children automatically touch the parent to maintain bitemporal consistency.

Define states as StateDefinition objects. Use meaningful signal enums (OrderSignal.SUBMIT). Transitions are TransitionDefinition objects: State + Signal -> New State + guard + action.

Encapsulate mutations as Action objects inheriting from AbstractSimpleAction. Execution is wrapped in inTx(db) for atomicity. Returns ActionResult with the output and its time coordinates.

Never use try/catch for business logic. Use runCatching { } or Result.failure(). Chain with flatMap, transform with map, handle at the edge with getOrElse or fold.


For standard entities needing Create, Read, Update, Delete, List, and History, use DataAuthorityEndpointNew.reify. This wires all six routes automatically given a service and a translator.

Routes produced:

  • POST /resource (Create)
  • GET /resource/{id} (Read)
  • PUT /resource (Update)
  • DELETE /resource (Delete)
  • POST /resource/query (List)
  • POST /resource/history (History)

Implement EndpointConfigurator directly for custom business actions (submit, approve). Use configureSecureRoutes(rt: Route) and register routes in the Ktor DSL. DTOs go in api/rest/types or inside the endpoint class.

The DSL uses a tree structure: Root > Group > Node > Leaf. Parameters are declared explicitly with withParameters { } to ensure OpenAPI documentation and type-safe extraction. The run { } block returns Result<T>; the framework maps success to 200/201 and failures to appropriate error codes via AppError normalization.

Use floatingNode to group related endpoints (e.g., summaryRoutes, detailsRoutes) into named sub-trees that can be composed in the serviceDefinition.

Tests are stored in api-test/collections organized by functional area matching the module structure. Use vars:post-response to capture IDs from responses and chain requests (Create -> Approve -> Submit).


A new module follows five layers in strict order:

  1. Business layer (business/): Domain entities (@Serializable @optics data class), enums for status, metadata types.
  2. Persistence layer (persistence/): TableConfiguration, ScopedTable definition, Universe class extending AbstractScopedUniverse.
  3. Service layer (service/): OrderLifecycle with StateDefinition, TransitionDefinition, and EngineDefinition.builder(). InternalService for domain logic.
  4. API layer (api/): EndpointConfigurator using DataAuthorityEndpointNew.reify or manual route definition.
  5. Module wiring (Module.kt): Application extension function that wires database, universe, service, lifecycle, endpoints, and calls MultiEndpointKtorModule(...).configureServer(this, registry).