Events¶
Katalyst's event system is split across two modules: katalyst-events defines the contracts
(DomainEvent, EventHandler, validation), and katalyst-events-bus provides the in-process
bus, transaction awareness, side effects, and deduplication. For a walkthrough, see
Publish and handle events.
Enable it with features { enableEvents() }.
DomainEvent¶
interface DomainEvent {
val eventId: String // default: derived per instance; used for deduplication
fun getMetadata(): EventMetadata // default supplied
fun eventType(): String // default: metadata.eventType
}
Events are immutable, past-tense facts. A plain data class is enough:
data class UserRegisteredEvent(
val accountId: Long,
val email: String,
val displayName: String
) : DomainEvent
eventId backs deduplication: a retried transaction re-publishing the same event id is not
delivered twice. EventMetadata carries the event type, correlation id, causation id,
timestamps, version, and source.
Event hierarchies¶
A handler registered for a base type receives every event assignable to it. Sealed classes are the usual way to group related events, and a handler of the sealed parent receives every concrete subtype:
sealed class UserEvent : DomainEvent
data class UserCreatedEvent(val id: Long) : UserEvent()
data class UserDeletedEvent(val id: Long) : UserEvent()
The same holds for an abstract class, an open class, an interface, or a leaf below a non-sealed
intermediate — and an EventHandler<DomainEvent> therefore acts as a catch-all that sees every
published event. A handler is invoked at most once per event, even when several of the types
it is registered under match.
EventHandler¶
Implement it and place the class under a scanned package; it is discovered and registered automatically. Contract:
- Multiple handlers may listen to the same event type.
- Handlers run asynchronously and in parallel.
- A handler that throws is logged, not propagated — other handlers still run.
- Handlers should be idempotent and complete reasonably quickly.
class UserRegistrationHandler(
private val userProfileService: UserProfileService
) : EventHandler<UserRegisteredEvent> {
override val eventType = UserRegisteredEvent::class
override suspend fun handle(event: UserRegisteredEvent) {
userProfileService.createProfileForAccount(event.accountId, event.displayName)
}
}
EventBus¶
interface EventBus {
suspend fun publish(event: DomainEvent)
// registration is performed at startup by the framework
}
Inject the bus to publish. Publishing finds every handler for the event type, runs them asynchronously, and returns when they complete; handler exceptions are caught and logged.
class AuthService(private val eventBus: EventBus) : Service {
suspend fun register(...) = transactionManager.transaction {
// …
eventBus.publish(UserRegisteredEvent(id, email, name))
}
}
Handlers are registered during startup auto-discovery — do not call register yourself.
Observe as a Flow¶
A Component can consume events reactively with the eventsOf<T>() extension instead of
implementing a handler:
ApplicationEventBus is the default implementation, exposing handler subscriptions over a
SharedFlow.
Transaction-aware publishing¶
When you publish inside transactionManager.transaction { … }, delivery is deferred until the
transaction commits; a rollback discards the pending events. TransactionAwareEventBus queues
events published inside a transaction; EventsTransactionAdapter — a TransactionAdapter the
framework registers automatically once enableEvents() is on — flushes them at commit
(SYNC_BEFORE_COMMIT, before commit; a handler failure rolls back the transaction) or after
commit (ASYNC_AFTER_COMMIT), and discards them on rollback. The publishPendingEvents
helper flushes events queued during a transaction.
This is what makes handlers safe: they never react to state that was rolled back.
Side effects¶
EventSideEffect wraps a DomainEvent for the generic transactional side-effect framework
(TransactionalSideEffect), so publishing can be scheduled as a SYNC_BEFORE_COMMIT or
ASYNC_AFTER_COMMIT side effect alongside other transactional work. It is a thin wrapper that
delegates to the event bus for the actual publish.
Deduplication¶
The bus deduplicates by eventId through an EventDeduplicationStore:
| Implementation | Behavior |
|---|---|
InMemoryEventDeduplicationStore |
Tracks seen event ids in memory. |
NoOpEventDeduplicationStore |
Disables deduplication. |
Interception, modes, and topology¶
| Type | Purpose |
|---|---|
EventBusInterceptor |
Hook beforePublish / afterPublish; an interceptor can abort a publish (InterceptResult). |
EventHandlingMode |
Controls how handlers are executed for an event. |
EventHandlerRegistry / InMemoryEventHandlerRegistry |
Where handlers are registered. |
EventTopology |
Wires handlers to the bus at startup. |
PublishResult / HandlerFailure |
The outcome of a publish and any per-handler failures. |
Validation¶
katalyst-events includes an event-validation layer: EventValidator,
BaseEventValidator, CompositeEventValidator, and NoOpEventValidator. The bus side adds
EventPublishingValidator (default DefaultEventPublishingValidator) to validate events
before publishing. Validation failures raise EventValidationException.
Exceptions¶
katalyst-events: EventException and subtypes (EventHandlerException,
EventPublishException, EventValidationException, EventSerializationException,
EventDeserializationException, EventRoutingException, EventConfigurationException,
EventHandlerRegistrationException).
katalyst-events-bus: EventPublishingException, HandlerException and subtypes
(HandlerExecutionException, HandlerRegistrationException, HandlerDiscoveryException,
InvalidHandlerMetadataException, WrongEventTypeException).
See also¶
- Publish and handle events
- Transactions — the adapter that defers delivery to commit.