From fdd9e615c8252c0668d25f15533f6d3d3a68b6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Thu, 26 Mar 2026 13:13:06 +0100 Subject: [PATCH 1/6] docs(core): add KDoc to public model classes (KOJAK-36) --- .../com/softwaremill/okapi/core/DeliveryResult.kt | 6 ++++++ .../kotlin/com/softwaremill/okapi/core/OutboxEntry.kt | 10 ++++++++++ .../kotlin/com/softwaremill/okapi/core/OutboxId.kt | 2 ++ .../com/softwaremill/okapi/core/OutboxMessage.kt | 1 + .../kotlin/com/softwaremill/okapi/core/OutboxStatus.kt | 2 ++ .../kotlin/com/softwaremill/okapi/core/RetryPolicy.kt | 2 ++ 6 files changed, 23 insertions(+) diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/DeliveryResult.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/DeliveryResult.kt index bdbf35a..ba4eafc 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/DeliveryResult.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/DeliveryResult.kt @@ -1,5 +1,11 @@ package com.softwaremill.okapi.core +/** + * Outcome of a single delivery attempt by a [MessageDeliverer]. + * + * [OutboxEntryProcessor] uses this to decide whether to retry, mark as failed, + * or mark as delivered. + */ sealed interface DeliveryResult { data object Success : DeliveryResult diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxEntry.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxEntry.kt index 19e1aef..6d20262 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxEntry.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxEntry.kt @@ -2,6 +2,12 @@ package com.softwaremill.okapi.core import java.time.Instant +/** + * Persistent representation of an outbox message with delivery state. + * + * Created via [createPending] and progressed through [retry], [toDelivered], + * or [toFailed] — each returning a new immutable copy. + */ data class OutboxEntry( val outboxId: OutboxId, val messageType: String, @@ -15,6 +21,7 @@ data class OutboxEntry( val lastError: String?, val deliveryMetadata: String, ) { + /** Returns a copy scheduled for another delivery attempt. */ fun retry(now: Instant, lastError: String): OutboxEntry = copy( status = OutboxStatus.PENDING, updatedAt = now, @@ -23,6 +30,7 @@ data class OutboxEntry( lastError = lastError, ) + /** Returns a copy marked as permanently failed. */ fun toFailed(now: Instant, lastError: String): OutboxEntry = copy( status = OutboxStatus.FAILED, updatedAt = now, @@ -30,6 +38,7 @@ data class OutboxEntry( lastError = lastError, ) + /** Returns a copy marked as successfully delivered. */ fun toDelivered(now: Instant): OutboxEntry = copy( status = OutboxStatus.DELIVERED, updatedAt = now, @@ -37,6 +46,7 @@ data class OutboxEntry( ) companion object { + /** Creates a new PENDING entry from a [message] and [deliveryInfo]. */ fun createPending(message: OutboxMessage, deliveryInfo: DeliveryInfo, now: Instant): OutboxEntry = createPending(message, deliveryType = deliveryInfo.type, deliveryMetadata = deliveryInfo.serialize(), now = now) diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxId.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxId.kt index 42c451c..b47a715 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxId.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxId.kt @@ -2,9 +2,11 @@ package com.softwaremill.okapi.core import java.util.UUID +/** Unique identifier for an outbox entry. Wraps a [UUID] for type safety. */ @JvmInline value class OutboxId(val raw: UUID) { companion object { + /** Generates a new random identifier. */ fun new(): OutboxId = OutboxId(UUID.randomUUID()) } } diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxMessage.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxMessage.kt index b1d78bc..3e63725 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxMessage.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxMessage.kt @@ -1,5 +1,6 @@ package com.softwaremill.okapi.core +/** Business message to be delivered via the transactional outbox. */ data class OutboxMessage( val messageType: String, val payload: String, diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxStatus.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxStatus.kt index ecf9a5f..3cf01bc 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxStatus.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/OutboxStatus.kt @@ -1,5 +1,6 @@ package com.softwaremill.okapi.core +/** Lifecycle status of an [OutboxEntry]. */ enum class OutboxStatus { PENDING, DELIVERED, @@ -7,6 +8,7 @@ enum class OutboxStatus { ; companion object { + /** Resolves a status by matching the given [value] against enum entry names. Throws if unknown. */ fun from(value: String): OutboxStatus = requireNotNull(entries.find { it.name == value }) { "Unknown outbox status: $value" } diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt index be704e8..8f73663 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt @@ -1,9 +1,11 @@ package com.softwaremill.okapi.core +/** Determines how many delivery attempts are allowed before an entry is marked as [OutboxStatus.FAILED]. */ data class RetryPolicy(val maxRetries: Int) { init { require(maxRetries >= 0) { "maxRetries must be >= 0, got: $maxRetries" } } + /** Returns `true` if [currentRetries] has not yet reached [maxRetries]. */ fun shouldRetry(currentRetries: Int): Boolean = currentRetries < maxRetries } From e160a4b5911aefbaee2e402bae150c3ccf233f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Thu, 26 Mar 2026 13:16:48 +0100 Subject: [PATCH 2/6] docs(http): add KDoc to HTTP transport classes (KOJAK-36) --- .../com/softwaremill/okapi/http/HttpDeliveryInfo.kt | 7 +++++++ .../okapi/http/HttpDeliveryInfoBuilder.kt | 12 ++++++++++++ .../softwaremill/okapi/http/HttpMessageDeliverer.kt | 10 ++++++++++ .../kotlin/com/softwaremill/okapi/http/HttpMethod.kt | 1 + 4 files changed, 30 insertions(+) diff --git a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfo.kt b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfo.kt index e15e600..f5d3a80 100644 --- a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfo.kt +++ b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfo.kt @@ -4,6 +4,12 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.softwaremill.okapi.core.DeliveryInfo +/** + * Delivery metadata for HTTP webhook transport. + * + * [serviceName] is resolved to a base URL via [ServiceUrlResolver] at delivery time. + * [endpointPath] is appended to form the full URL. + */ data class HttpDeliveryInfo( override val type: String = TYPE, val serviceName: String, @@ -22,6 +28,7 @@ data class HttpDeliveryInfo( const val TYPE = "http" private val mapper = jacksonObjectMapper() + /** Deserializes from JSON stored in [OutboxEntry.deliveryMetadata]. */ fun deserialize(json: String): HttpDeliveryInfo = mapper.readValue(json) } } diff --git a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfoBuilder.kt b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfoBuilder.kt index d3abcea..80c9cee 100644 --- a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfoBuilder.kt +++ b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpDeliveryInfoBuilder.kt @@ -22,4 +22,16 @@ class HttpDeliveryInfoBuilder { ) } +/** + * DSL for building [HttpDeliveryInfo]. + * + * ``` + * val info = httpDeliveryInfo { + * serviceName = "order-service" + * endpointPath = "/api/events" + * httpMethod = HttpMethod.POST + * header("X-Correlation-Id", correlationId) + * } + * ``` + */ fun httpDeliveryInfo(block: HttpDeliveryInfoBuilder.() -> Unit): HttpDeliveryInfo = HttpDeliveryInfoBuilder().apply(block).build() diff --git a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMessageDeliverer.kt b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMessageDeliverer.kt index 11b8129..a67f409 100644 --- a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMessageDeliverer.kt +++ b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMessageDeliverer.kt @@ -9,6 +9,16 @@ import java.net.http.HttpRequest import java.net.http.HttpResponse import java.time.Duration +/** + * [MessageDeliverer] that sends outbox entries as HTTP requests via JDK [HttpClient]. + * + * Status code classification: + * - 2xx → [DeliveryResult.Success] + * - 5xx, 429, 408 → [DeliveryResult.RetriableFailure] (configurable via [retriableStatusCodes]) + * - other → [DeliveryResult.PermanentFailure] + * + * Connection errors are treated as retriable. + */ class HttpMessageDeliverer( private val urlResolver: ServiceUrlResolver, private val httpClient: HttpClient = defaultHttpClient(), diff --git a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMethod.kt b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMethod.kt index 877778e..2ff6bc4 100644 --- a/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMethod.kt +++ b/okapi-http/src/main/kotlin/com/softwaremill/okapi/http/HttpMethod.kt @@ -1,5 +1,6 @@ package com.softwaremill.okapi.http +/** HTTP methods supported by [HttpMessageDeliverer]. */ enum class HttpMethod { POST, PUT, From 01785639f9a9de2d4b617e28520be846d59c1821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Thu, 26 Mar 2026 13:20:24 +0100 Subject: [PATCH 3/6] docs(kafka): add KDoc to Kafka transport classes (KOJAK-36) --- .../com/softwaremill/okapi/kafka/KafkaDeliveryInfo.kt | 7 +++++++ .../okapi/kafka/KafkaDeliveryInfoBuilder.kt | 11 +++++++++++ .../softwaremill/okapi/kafka/KafkaMessageDeliverer.kt | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfo.kt b/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfo.kt index e945c5a..38bd713 100644 --- a/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfo.kt +++ b/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfo.kt @@ -4,6 +4,12 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.softwaremill.okapi.core.DeliveryInfo +/** + * Delivery metadata for Kafka topic transport. + * + * [topic] is required. Optional [partitionKey] controls partition routing. + * Custom [headers] are sent as UTF-8 encoded Kafka record headers. + */ data class KafkaDeliveryInfo( override val type: String = TYPE, val topic: String, @@ -20,6 +26,7 @@ data class KafkaDeliveryInfo( const val TYPE = "kafka" private val mapper = jacksonObjectMapper() + /** Deserializes from JSON stored in [OutboxEntry.deliveryMetadata]. */ fun deserialize(json: String): KafkaDeliveryInfo = mapper.readValue(json) } } diff --git a/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfoBuilder.kt b/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfoBuilder.kt index 1cbc99e..544bc1f 100644 --- a/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfoBuilder.kt +++ b/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaDeliveryInfoBuilder.kt @@ -20,4 +20,15 @@ class KafkaDeliveryInfoBuilder { ) } +/** + * DSL for building [KafkaDeliveryInfo]. + * + * ``` + * val info = kafkaDeliveryInfo { + * topic = "order-events" + * partitionKey = orderId + * header("source", "checkout-service") + * } + * ``` + */ fun kafkaDeliveryInfo(block: KafkaDeliveryInfoBuilder.() -> Unit): KafkaDeliveryInfo = KafkaDeliveryInfoBuilder().apply(block).build() diff --git a/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaMessageDeliverer.kt b/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaMessageDeliverer.kt index 765769f..31b99b2 100644 --- a/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaMessageDeliverer.kt +++ b/okapi-kafka/src/main/kotlin/com/softwaremill/okapi/kafka/KafkaMessageDeliverer.kt @@ -8,6 +8,13 @@ import org.apache.kafka.clients.producer.ProducerRecord import org.apache.kafka.common.errors.RetriableException import java.util.concurrent.ExecutionException +/** + * [MessageDeliverer] that publishes outbox entries to Kafka topics. + * + * Uses the provided [Producer] to send records synchronously. + * Kafka [RetriableException]s map to [DeliveryResult.RetriableFailure]; + * all other errors map to [DeliveryResult.PermanentFailure]. + */ class KafkaMessageDeliverer( private val producer: Producer, ) : MessageDeliverer { From a351b6beb3780e71261f43d5c03a77abdbed44b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Thu, 26 Mar 2026 13:23:14 +0100 Subject: [PATCH 4/6] docs(stores): add KDoc to Postgres and MySQL store classes (KOJAK-36) --- .../main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt | 1 + .../com/softwaremill/okapi/postgres/PostgresOutboxStore.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt b/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt index fb9940b..d3559dc 100644 --- a/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt +++ b/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt @@ -20,6 +20,7 @@ import java.time.Clock import java.time.Instant import java.util.UUID +/** MySQL [OutboxStore] implementation using Exposed ORM. */ class MysqlOutboxStore( private val clock: Clock, ) : OutboxStore { diff --git a/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt b/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt index f7ca77f..6483da8 100644 --- a/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt +++ b/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt @@ -20,6 +20,7 @@ import java.time.Clock import java.time.Instant import java.util.UUID +/** PostgreSQL [OutboxStore] implementation using Exposed ORM. */ class PostgresOutboxStore( private val clock: Clock, ) : OutboxStore { From 72100fe679d49a90b5705486d9919ad3bf1d70ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Thu, 26 Mar 2026 14:13:34 +0100 Subject: [PATCH 5/6] =?UTF-8?q?fix(docs):=20clarify=20RetryPolicy=20KDoc?= =?UTF-8?q?=20=E2=80=94=20retries,=20not=20total=20attempts=20(KOJAK-36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt index 8f73663..b0052b0 100644 --- a/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt +++ b/okapi-core/src/main/kotlin/com/softwaremill/okapi/core/RetryPolicy.kt @@ -1,6 +1,6 @@ package com.softwaremill.okapi.core -/** Determines how many delivery attempts are allowed before an entry is marked as [OutboxStatus.FAILED]. */ +/** Determines how many retries are allowed after the initial delivery attempt before an entry is marked as [OutboxStatus.FAILED]. */ data class RetryPolicy(val maxRetries: Int) { init { require(maxRetries >= 0) { "maxRetries must be >= 0, got: $maxRetries" } From dd92cfb91136ef6e31c9fce80f7372515375d36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Thu, 26 Mar 2026 14:36:20 +0100 Subject: [PATCH 6/6] =?UTF-8?q?fix(docs):=20Exposed=20is=20not=20an=20ORM?= =?UTF-8?q?=20=E2=80=94=20drop=20misleading=20label=20from=20store=20KDoc?= =?UTF-8?q?=20(KOJAK-36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt | 2 +- .../com/softwaremill/okapi/postgres/PostgresOutboxStore.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt b/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt index d3559dc..bd7e12e 100644 --- a/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt +++ b/okapi-mysql/src/main/kotlin/com/softwaremill/okapi/mysql/MysqlOutboxStore.kt @@ -20,7 +20,7 @@ import java.time.Clock import java.time.Instant import java.util.UUID -/** MySQL [OutboxStore] implementation using Exposed ORM. */ +/** MySQL [OutboxStore] implementation using Exposed. */ class MysqlOutboxStore( private val clock: Clock, ) : OutboxStore { diff --git a/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt b/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt index 6483da8..e740eef 100644 --- a/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt +++ b/okapi-postgres/src/main/kotlin/com/softwaremill/okapi/postgres/PostgresOutboxStore.kt @@ -20,7 +20,7 @@ import java.time.Clock import java.time.Instant import java.util.UUID -/** PostgreSQL [OutboxStore] implementation using Exposed ORM. */ +/** PostgreSQL [OutboxStore] implementation using Exposed. */ class PostgresOutboxStore( private val clock: Clock, ) : OutboxStore {