Service Implementation Requirements Best Practices
This document provides guidance on writing high-quality service implementation requirements for Arda’s backend Kotlin services. Follow these practices when creating or reviewing a Service Implementation Requirements document.
Architectural Context
Section titled “Architectural Context”Module Structure
Section titled “Module Structure”Every service module follows a consistent package structure:
cards.arda.operations.<domain>.<module>/├── Module.kt # Entry point and DI wiring├── api/ # REST endpoint definitions│ └── rest/<Entity>Endpoint.kt├── business/ # Business entities (sealed interfaces + data classes)│ ├── <Entity>.kt│ └── <Entity>Metadata.kt├── domain/ # Value types, enums, domain logic│ ├── <ValueType>.kt│ └── persistence/ # Column component definitions├── persistence/ # Database layer│ ├── <Entity>Persistence.kt # Table + Record definitions│ └── <Entity>Universe.kt # Persistence operations└── service/ # Service interface and implementation └── <Entity>Service.ktKey Interfaces and Base Classes
Section titled “Key Interfaces and Base Classes”| Interface/Class | Purpose | When to Use |
|---|---|---|
EntityPayload | Base for all entities with eId | All business entities |
ScopedMetadata | Metadata with tenant scoping | Root entities |
ChildMetadata | Metadata with parent reference | Child entities |
EditableDataAuthorityService<E, M> | CRUD service contract | Services managing entities |
ScopedTable | Base table with tenant support | All entity tables |
ScopedRecord<E, M, T, R> | ORM record with bitemporal support | All entity records |
ScopedUniverse | Persistence operations wrapper | Encapsulating DB operations |
Core Principles
Section titled “Core Principles”1. Sealed Interface + Entity Pattern
Section titled “1. Sealed Interface + Entity Pattern”All business entities should use the sealed interface pattern:
@Serializable(with = EntitySerializer::class)sealed interface Entity : EntityPayload { override val eId: EntityId val name: String
@Serializable data class Entity( override val eId: EntityId, override val name: String, ) : Entity { override fun validate(ctx: ApplicationContext, mutation: Mutation): Result<Unit> { // Validation logic returning AppError.Composite on failure } }}Rationale: This pattern enables polymorphic serialization and future extension.
2. Result-Based Error Handling
Section titled “2. Result-Based Error Handling”All methods that can fail must return Result<T>:
- Success path:
Result.success(value) - Failure path:
Result.failure(AppError.<Type>(...)) - Chaining: Use
flatMap,map,onSuccess,onFailure - Collection operations: Use
collectAll()forList<Result<T>>
Avoid throwing exceptions:
// Badsuspend fun create(entity: Entity): Entity { if (entity.name.isBlank()) throw IllegalArgumentException("Name required") return repository.save(entity)}
// Goodsuspend fun create(entity: Entity, ...): Result<EntityRecord<E, M>> { return entity.validate(ctx, Mutation.CREATE).flatMap { universe.create(entity, metadata, asOf, author)() }}3. Mutation Qualifiers
Section titled “3. Mutation Qualifiers”Document behavior for all three qualifiers for operations that accept them:
| Qualifier | Intent | Validation | Side Effects |
|---|---|---|---|
| LAX | Flexibility, imports | Minimal | Upsert behavior |
| STRICT | Data integrity, UI | Full consistency check | No implicit changes |
| PROPAGATE | Cascade changes | Relaxed | Updates related entities |
Always include a “Behavior by Qualifier” table in every method specification.
4. Bitemporal Awareness
Section titled “4. Bitemporal Awareness”All persistence operations must account for bitemporal storage:
time: Long: Effective time for the operationTimeCoordinates.now(time): Standard construction patternasOfqueries: Point-in-time reads
Entity Definition Best Practices
Section titled “Entity Definition Best Practices”Property Guidelines
Section titled “Property Guidelines”- Use value types for complex properties:
Quantity.Valueinstead of separateamount+unit. - Required fields: non-nullable with validation. Optional fields: nullable with defaults in
Entitydata class. - Entity references: Use
UUID(asEntityId). Pair with optional name/display field for denormalization. - Enum values: Place in
domain/package. IncludeUNKNOWNas first value for forward compatibility.
Validation Rules
Section titled “Validation Rules”Collect all validation errors before returning:
override fun validate(ctx: ApplicationContext, mutation: Mutation): Result<Unit> { val errors = mutableListOf<AppError>()
if (name.isBlank()) errors.add(AppError.ArgumentValidation("name", "Name is required"))
return if (errors.isEmpty()) Result.success(Unit) else Result.failure(AppError.Composite("<Entity> Validation Failed", null, errors))}Service Interface Best Practices
Section titled “Service Interface Best Practices”Method Signature Patterns
Section titled “Method Signature Patterns”| Operation | Standard Signature |
|---|---|
| Create | suspend fun create(payload: E, metadata: M, time: Long, author: String, qualifier: Qualifier): Result<EntityRecord<E, M>> |
| Update | suspend fun update(payload: E, metadata: M, time: Long, author: String, qualifier: Qualifier): Result<EntityRecord<E, M>> |
| Delete | suspend fun delete(eId: UUID, time: Long, author: String): Result<EntityRecord<E, M>> |
| Get | suspend fun getAsOf(eId: UUID, asOf: TimeCoordinates): Result<EntityRecord<E, M>?> |
| List | suspend fun listAsOf(filter: Filter, asOf: TimeCoordinates): Result<PageResult<E, M>> |
Implementation Class Structure
Section titled “Implementation Class Structure”class Impl( override val draftStore: DraftStore<E, M>, override val universe: <Entity>Universe, private val dependency: DependencyService, override val db: Database,) : <Entity>Service, ObserverManager<...> by InMemoryObserverManagerDelegate(), LogEnabled by LogProvider(Impl::class) {
override val serviceId = TypeId()
override suspend fun create(...): Result<EntityRecord<E, M>> = inTransaction(db) { // Implementation }}Transaction Boundaries
Section titled “Transaction Boundaries”- Wrap operations in
inTransaction(db) { ... }orinTransaction(db, readOnly = true) { ... } - Document which operations are transactional in method specifications
Persistence Best Practices
Section titled “Persistence Best Practices”Column Naming
Section titled “Column Naming”Use lowercase snake_case. Use component columns for complex value types:
val orderQuantity = root.quantityComponent<...>("order_quantity")// → order_quantity_amount, order_quantity_unitRecord Class Pattern
Section titled “Record Class Pattern”class EntityRecord(rId: EntityID<UUID>) : ScopedRecord<E, M, TABLE, EntityRecord>(rId, TABLE) { var property: Type by TABLE.property
companion object : Persistence<E, M, TABLE, EntityRecord>( TABLE, EntityRecord::class.java, { EntityRecord(it) }, { insertStm, payload, metadata -> insertStm[property] = payload.property // Map all properties } )
override fun fillPayload() { payload = Entity.Entity( eId = eId, property = property, // Map all properties ) }
override fun fillMetadata() { metadata = EntityMetadata(tenantId) }}fillPayload() maps database columns back to the business entity. fillMetadata() reconstructs the metadata object (e.g., tenant scope, parent reference). Both methods must be implemented for every record class.
Universe Pattern
Section titled “Universe Pattern”The Universe encapsulates all persistence operations for a single entity type:
class EntityUniverse : ScopedUniverse<E, M, TABLE, EntityRecord>( TABLE, EntityRecord, EntityUniversalCondition) { // Custom queries go here suspend fun findByName(name: String, asOf: TimeCoordinates): Result<BiTemporalEntity<E, M>?> = ...}Custom queries beyond standard CRUD belong in the Universe class, not in the service.
Safe Migration Sequence
Section titled “Safe Migration Sequence”When adding columns to existing tables:
- Add nullable column first.
- Backfill existing data.
- Add NOT NULL constraint after confirming all data is backfilled.
Module Configuration Best Practices
Section titled “Module Configuration Best Practices”Entry Point Function
Section titled “Entry Point Function”fun Application.<module>( cfgProvider: ConfigurationProvider, authentication: Authentication, // ... other dependencies): <Entity>Service { val moduleCfg = cfgProvider.moduleConfiguration("system.<domain>.<module>")
// Validate required configuration val dsCfg = moduleCfg.dataSource ?: throw AppError.ArgumentValidation("cfg.dataSource", "DataSource is required for <Module> Module")
// Initialize database val db: Database = DataSource(dsCfg.db, dsCfg.pool).newDb(dsCfg.flywayConfig)
// Construct dependencies val universe = <Entity>Universe() val service = <Entity>Service.Impl(universe, db, ...)
// Configure endpoints val endpoint = <Entity>Endpoint.Impl(moduleCfg, locator, service) val ktorModule = MultiEndpointKtorModule(...) ktorModule.configureServer(this)
return service}Dependency Injection for Testing
Section titled “Dependency Injection for Testing”Support test injection with nullable constructor parameters:
fun Application.<module>Module( ..., injectedUniverse: <Entity>Universe? = null, injectedService: <Entity>Service? = null): <Entity>Service { val activeUniverse = injectedUniverse ?: <Entity>Universe() val service = injectedService ?: <Entity>Service.Impl(activeUniverse, db, ...)}Review Checklist
Section titled “Review Checklist”Entity Definitions
Section titled “Entity Definitions”- Sealed interface pattern: Uses
sealed interface+Entitydata class - Validation method: Implements
validate()returningResult<Unit> - Error aggregation: Validation collects all errors into
AppError.Composite
Service Interface
Section titled “Service Interface”- Result return types: All fallible methods return
Result<T> - Suspend functions: All I/O operations are suspend
- Qualifier documentation: All three behaviors documented per method
Method Specifications
Section titled “Method Specifications”- Pre-conditions defined
- Error conditions mapped: All failure cases mapped to
AppErrortypes - Side effects listed
- Transaction scope clear
Persistence Layer
Section titled “Persistence Layer”- Column naming: Snake_case matching conventions
- Component columns: Complex types use components
- Fill methods: Both
fillPayload()andfillMetadata()implemented - Universe queries: Custom queries encapsulated in Universe class
Testing Coverage
Section titled “Testing Coverage”- Unit tests specified: Coverage for validation, each qualifier
- Integration tests: Database round-trip testing identified
- Harness structure: Clear test setup pattern documented
Common Anti-Patterns
Section titled “Common Anti-Patterns”Throwing Exceptions Instead of Returning Failures
Section titled “Throwing Exceptions Instead of Returning Failures”Bad: Using throw NotFoundException(...) instead of Result.failure(AppError.NotFound(...))
Missing Qualifier Behavior Documentation
Section titled “Missing Qualifier Behavior Documentation”Bad: A method spec that says “Creates the entity” without differentiating LAX/STRICT/PROPAGATE.
Inconsistent Error Types
Section titled “Inconsistent Error Types”Bad: Using IllegalArgumentException, RuntimeException, or generic Exception.
Good: Using the AppError hierarchy consistently.
Hardcoded Column Names in Filters
Section titled “Hardcoded Column Names in Filters”Bad:
val filter = Filter.Eq("name", value)Good:
val filter = Filter.Eq(TABLE.name.name, value)Referencing the column object directly ensures refactoring is safe and eliminates typos.
Multiple Return Statements
Section titled “Multiple Return Statements”Bad:
suspend fun get(id: UUID): Result<Entity?> { val entity = repository.find(id) if (entity == null) return Result.success(null) if (!entity.isActive) return Result.failure(AppError.IncompatibleState("...")) return Result.success(entity)}Good:
suspend fun get(id: UUID): Result<Entity?> { return repository.find(id).flatMap { entity -> when { entity == null -> Result.success(null) !entity.isActive -> Result.failure(AppError.IncompatibleState("...")) else -> Result.success(entity) } }}Prefer functional chaining with flatMap over early return statements to keep error handling consistent.
Integration with Development Workflow
Section titled “Integration with Development Workflow”From Requirements to Implementation
Section titled “From Requirements to Implementation”Each requirement section maps to a specific implementation artifact:
- Entity Definitions (ED-*) → Implement
business/<Entity>.kt - Value Types (VT-*) → Implement
domain/<ValueType>.kt - Persistence Specs (PS-*) → Implement
persistence/<Entity>Persistence.ktand<Entity>Universe.kt - Service Interface (SI-*) → Implement
service/<Entity>Service.kt - Method Specs (MS-*) → Implement each method in
Service.Impl - Module Config (MC-*) → Implement
Module.kt
Linking to Tests
Section titled “Linking to Tests”Reference requirement IDs in test names:
@Testfun `MS-ITM-003 - update with STRICT qualifier fails when supply name mismatches`() { ... }
@Testfun `VR-001 - entity validation fails when name is blank`() { ... }Related Documents
Section titled “Related Documents”Templates
Section titled “Templates”- Service Implementation Requirements — the template this document accompanies
- Requirements List Template — for documenting feature requirements before implementation
- Incremental Service Requirements Template — for modifying existing services
Best Practices
Section titled “Best Practices”- Requirements List Best Practices — guidance for writing feature requirements
- Incremental Service Best Practices — guidance for service modifications
Reference
Section titled “Reference”- Kotlin coding standards — see
kotlin-codingskill - Testing patterns and practices — see
unit-testsskill - Task Workflow — see
implementation-taskskill
Copyright: © Arda Systems 2025-2026, All rights reserved