Architecture Explorations
Exploratory documents capturing architectural patterns, comparisons, and conventions used across the Arda backend. These are design references, not active project specifications.
Arda vs. Textbook Enterprise Patterns
Section titled “Arda vs. Textbook Enterprise Patterns”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.
Data Access — Universe Pattern vs. ORM
Section titled “Data Access — Universe Pattern vs. ORM”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
EntityPayloaddata classes eliminate mutation bugs. - Trade-off: More boilerplate:
Table,Record,Persistence, andUniversedefinitions 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.
Guardblocks 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:
flatMapchains 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.
Arda Coding Patterns
Section titled “Arda Coding Patterns”Immutability and Domain Modeling
Section titled “Immutability and Domain Modeling”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.
Bitemporality (Universe Pattern)
Section titled “Bitemporality (Universe Pattern)”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.
One-to-Many Relationships (ChildUniverse)
Section titled “One-to-Many Relationships (ChildUniverse)”Use ChildUniverse for entities with child records (e.g., Order to OrderLines). Updates to children automatically touch the parent to maintain bitemporal consistency.
State Management (Engine Pattern)
Section titled “State Management (Engine Pattern)”Define states as StateDefinition objects. Use meaningful signal enums (OrderSignal.SUBMIT). Transitions are TransitionDefinition objects: State + Signal -> New State + guard + action.
Command Execution (Action Pattern)
Section titled “Command Execution (Action Pattern)”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.
Functional Error Handling
Section titled “Functional Error Handling”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.
REST API Development Guide
Section titled “REST API Development Guide”Standard CRUD — Data Authority DSL
Section titled “Standard CRUD — Data Authority DSL”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)
Custom Endpoints
Section titled “Custom Endpoints”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.
Endpoint DSL
Section titled “Endpoint DSL”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.
Bruno API Tests
Section titled “Bruno API Tests”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).
Implementing a Business Domain Module
Section titled “Implementing a Business Domain Module”A new module follows five layers in strict order:
- Business layer (
business/): Domain entities (@Serializable @optics data class), enums for status, metadata types. - Persistence layer (
persistence/):TableConfiguration,ScopedTabledefinition,Universeclass extendingAbstractScopedUniverse. - Service layer (
service/):OrderLifecyclewithStateDefinition,TransitionDefinition, andEngineDefinition.builder().InternalServicefor domain logic. - API layer (
api/):EndpointConfiguratorusingDataAuthorityEndpointNew.reifyor manual route definition. - Module wiring (
Module.kt):Applicationextension function that wires database, universe, service, lifecycle, endpoints, and callsMultiEndpointKtorModule(...).configureServer(this, registry).
Copyright: © Arda Systems 2025-2026, All rights reserved