Endpoint Definition DSL
Arda endpoints are wired to Ktor through one of two routes, depending on whether the surface fits the CRUDQ resource model or not. This page describes both routes, the DSL blocks used in the lightweight path, and how they are mounted at start-up.
When to use DataAuthorityEndpoint
Section titled “When to use DataAuthorityEndpoint”Derive from DataAuthorityEndpoint when the surface is a first-class data-authority resource managed through the bitemporal framework. Concretely, that means all five standard operations are in scope:
- Create — mint a new entity.
- Read by eId — retrieve the current state of an entity by its entity identifier.
- Read as-of — retrieve the state of an entity at a specific point in time.
- Update — apply a mutating payload to an existing entity.
- Query — filter/sort/paginate the entity collection.
If the surface has all five of these operations and maps cleanly to a single Universe<EP, M>, use DataAuthorityEndpoint. See Data Authority Pattern for the four-layer module structure that DataAuthorityEndpoint sits at the top of.
When to use the lightweight DSL
Section titled “When to use the lightweight DSL”Use the serviceDefinition DSL for any surface that does not fit CRUDQ:
- POST + GET pairs without bitemporal read semantics.
- Webhook ingestion endpoints (POST only).
- Async job submission (POST to enqueue, GET to poll status).
- Any surface whose request/response types are not
Universe-backed entity payloads.
Worked references:
CsvUploadRoutes— existing non-CRUDQ precedent.EmailJobEndpoint— job submission (POST) and status query (GET), with idempotency on the POST path.PostmarkEventsEndpoint— webhook ingestion (POST), unauthenticated inbound surface.
serviceDefinition block structure
Section titled “serviceDefinition block structure”A serviceDefinition block declares the endpoint’s identity and security posture, then enumerates its HTTP operations via a forService sub-block.
val definition = serviceDefinition( moduleName = "email", // logical module name; used in route prefixes specId = "email-jobs", // stable spec identifier for the OpenAPI registry secure = true, // false for unauthenticated surfaces (e.g., webhooks)) { forService("email-jobs") { post<CreateEmailJobRequest, CreateEmailJobResponse>( path = "/email-jobs", summary = "Submit an email job", ) { responds(createJobBodyMessage) { withParameters(idempotencyKeyParam) { run { req, params -> emailJobService.create(req, params.idempotencyKey) } } } }
get<EmailJobStatusResponse>( path = "/email-jobs/{jobId}", summary = "Get email job status", ) { responds(jobStatusBodyMessage) { run { _, params -> emailJobService.getStatus(params.jobId) } } } }}The three required arguments at the serviceDefinition level:
| Argument | Type | Purpose |
|---|---|---|
moduleName | String | Logical module name; contributes to the route prefix. |
specId | String | Stable identifier registered with the OpenAPI spec registry. Must be unique per component. |
secure | Boolean | true applies the component’s configured Authentication; false marks the surface as unauthenticated. |
Body message declarations
Section titled “Body message declarations”Each responds(...) call takes a body message that names and describes the response type. Two constructors cover the common cases.
Reified constructor for directly @Serializable types:
val jobStatusBodyMessage = RequiredBodyMessage<EmailJobStatusResponse>("email-job-status") { summary = "Email job status" description = "Current status and metadata for an email job."}Four-argument constructor for types that need a point-of-use KSerializer:
Some types are not directly @Serializable — either because the type is a sealed class whose serializer requires a custom KSerializer at the construction site, or because the type lives in a library that does not yet carry @Serializable (see Idempotency — Serialization for the TypedIdempotencyOutcome case):
val idempotencyOutcomeBodyMessage = RequiredBodyMessage( name = "email-job-idempotency-outcome", kType = typeOf<EmailJobIdempotencyOutcome>(), kSerializer = EmailJobIdempotencyOutcomeSerializer, typeInfo = typeInfo<EmailJobIdempotencyOutcome>(),)The four-argument form sidesteps reified resolution and pins the serializer explicitly at the call site. See EmailJobIdempotencyOutcomeSerializer in cards.arda.operations.shopaccess.email.api.rest for the reference implementation.
MultiEndpointKtorModule mounting
Section titled “MultiEndpointKtorModule mounting”Endpoints defined with the serviceDefinition DSL are collected into a MultiEndpointKtorModule and mounted in the module entry-point:
fun Application.email( cfgProvider: ConfigurationProvider, authentication: Authentication, registry: ModuleRegistry,): EmailServices { // ... service construction ...
MultiEndpointKtorModule( component = cfgProvider.component(), moduleConfig = cfg, authentication = authentication, endpoints = listOf(emailJobEndpoint, postmarkEventsEndpoint), ).configureServer(this, registry)
return EmailServices(/* ... */)}The four constructor arguments:
| Argument | Purpose |
|---|---|
component | Component identity; used for route prefix and OpenAPI metadata. |
moduleConfig | Module-level configuration (base path, feature flags, etc.). |
authentication | Authentication instance from the canonical entry-point parameter. |
endpoints | List of serviceDefinition-backed endpoint instances to mount. |
configureServer(application, registry) registers all routes and publishes the module to the OpenAPI spec registry in one call.
Decision rubric
Section titled “Decision rubric”| Situation | Use |
|---|---|
CRUDQ resource backed by a Universe<EP, M> | DataAuthorityEndpoint |
| POST + GET without bitemporal semantics, or webhook ingest | serviceDefinition DSL + MultiEndpointKtorModule |
| Unauthenticated inbound surface (webhooks, health checks) | serviceDefinition with secure = false |
Related pages
Section titled “Related pages”- Idempotency — the
TypedIdempotencyOutcometype and itsKSerializerworkaround referenced in “Body message declarations”. - L1 Proxy Pattern — the transport layer that endpoint handlers call.
- Module Wiring Entry Point — where
MultiEndpointKtorModuleis mounted inModule.kt.
Copyright: © Arda Systems 2025-2026, All rights reserved