Events System
The EDK's event system provides a structured way to observe what happens in your application, party creation, identity verification, authorization decisions, database connections, configuration changes, without coupling the code that produces events to the code that consumes them.
Events flow through a central EventHub (pub/sub bus) where any number of subscribers can react: logging handlers, audit trail writers, analytics pipelines, webhook transmitters, or the Shared Signals Framework for cross-domain security event exchange.
Why Events?
In a command-oriented architecture, most important state changes happen inside commands. Without events, the only way to know what happened is to read the command's return value at the call site. But many systems need to know about state changes, audit logs, analytics, caches, search indexes, external notifications, and none of them should be called directly from the command implementation.
Events decouple producers from consumers. The PartyService emits a PARTY_CREATED event and moves on. Whether that event gets written to an audit log, forwarded to Kafka, or broadcast as an OpenID Shared Signal is a deployment-time decision, not a development-time one. Adding a new consumer doesn't require changing the producer.
Event Structure
Every event carries a standard set of fields from the IDK's core event model:
- Type: what happened (e.g.,
PARTY_CREATED,IDENTITY_VERIFIED,AUTHZ_DENIED) - Subsystem: which domain produced it (e.g.,
PARTY,IDENTITY,AUTHZ,SETTINGS) - Category: broad classification (
LIFECYCLE,SECURITY,CONFIGURATION) - Payload: event-specific data as a typed map
- Session context: tenant ID, principal ID, session ID, correlation ID for tracing
- Timestamp: when the event occurred
- Optional signature: KMS-signed events for tamper evidence
Event Types
The EDK defines event types for each domain:
Identity & Party
| Event | Subsystem | When it fires |
|---|---|---|
PARTY_CREATED / UPDATED / DELETED | PARTY | Party lifecycle changes |
IDENTITY_CREATED / UPDATED / DELETED | IDENTITY | Identity lifecycle changes |
IDENTITY_VERIFIED | IDENTITY | Identity verification completed |
CONTACT_CREATED / UPDATED / DELETED | CONTACT | Contact information changes |
Tenant & Configuration
| Event | Subsystem | When it fires |
|---|---|---|
TENANT_CREATED / UPDATED / DELETED | TENANT | Tenant lifecycle changes |
TENANT_SUSPENDED / TENANT_ACTIVATED | TENANT | Tenant state transitions |
SETTINGS_CREATED / UPDATED / DELETED | SETTINGS | Configuration changes |
Authorization & Security
| Event | Subsystem | When it fires |
|---|---|---|
AUTHZ_GRANTED | AUTHZ | Policy evaluation returned PERMIT |
AUTHZ_DENIED | AUTHZ | Policy evaluation returned DENY |
AUTHZ_ERROR | AUTHZ | Policy evaluation failed (PDP unreachable, etc.) |
Infrastructure
| Event | Subsystem | When it fires |
|---|---|---|
DB_CONNECTION_ACQUIRED / RELEASED / FAILED | DB_ROUTING | Database connection pool events |
Subscribing to Events
The EventHub provides a reactive subscription API using Kotlin Flow. Subscribers can filter events by type, subsystem, category, or custom predicates:
// Subscribe to all identity events
eventHub.subscribe(EdkEventFilter.forSubsystem(EdkEventSubsystems.IDENTITY))
.collect { event ->
logger.info("Identity event: ${event.type} for ${event.payload["identityId"]}")
}
// Subscribe to authorization denials for a specific tenant
eventHub.subscribe(edkEventFilter {
subsystem(EdkEventSubsystems.AUTHZ)
type(EdkEventTypes.AUTHZ_DENIED)
tenantId("acme-corp")
}).collect { event ->
alertService.notifyDenial(event)
}
The EdkEventFilter extends the IDK's base EventFilter with EDK-specific dimensions, tenant ID, principal ID, session ID, time ranges, and correlation IDs. Filters can be composed with and() for complex subscription criteria.
Event Transmission
Events that need to leave the application, to Kafka, webhooks, or external systems, go through EventTransmitter implementations. Transmitters are registered with the EventTransmitterRegistry and can be filtered so each transmitter only receives the events it cares about.
interface EventTransmitter {
val id: String
val isEnabled: Boolean
suspend fun transmit(event: Event): IdkResult<TransmissionResult, IdkError>
suspend fun transmitBatch(events: List<Event>): IdkResult<List<TransmissionResult>, IdkError>
}
Each transmission is tracked with a lifecycle:
Failed transmissions are retried with configurable delay and exponential backoff. After the maximum retry count is exceeded, the transmission moves to the dead letter queue for manual investigation. The EventTransmissionRepository persists all transmission records so nothing is silently lost.
Forwarding configuration per transmitter:
data class EventForwardingConfig(
val transmitterId: String,
val filter: EventFilter = EventFilter.ALL,
val enabled: Boolean = true,
val maxRetries: Int = 3,
val retryDelayMs: Long = 1000,
val exponentialBackoff: Boolean = true
)
Event Persistence
Events can be persisted to a database for querying, compliance, and historical analysis. The EventRepository provides tenant-scoped CRUD with pagination:
interface EventRepository {
suspend fun store(tenantId: String, event: Event): IdkResult<Event, IdkError>
suspend fun findAll(tenantId: String, filter: EventQueryFilter): EventPage<Event>
suspend fun findByCorrelationId(tenantId: String, correlationId: String): List<Event>
suspend fun findBySessionId(tenantId: String, sessionId: String, filter: EventQueryFilter): EventPage<Event>
suspend fun count(tenantId: String, filter: EventQueryFilter): Long
suspend fun deleteOlderThan(tenantId: String, olderThan: Instant): Long
}
PostgreSQL is the primary persistent backend, with MySQL and SQLite also available. The EventQueryFilter supports filtering by type, subsystem, category, time range, correlation ID, session ID, and principal, with pagination via offset/limit.
Retention
Persisted events accumulate over time. The EventRetentionJob automatically cleans up events older than the configured retention period (default 90 days) on a cron schedule:
sphereon:
events:
store:
type: postgresql
retention-days: 90
retention-cron: "0 0 2 * * *" # Daily at 2 AM
retention-enabled: true
The retention job also cleans up successful transmission records and expired dead letters.
Event Signing
For environments that require tamper evidence, events can be KMS-signed at creation time. The signature is stored alongside the event and can be verified later to confirm the event hasn't been modified:
sphereon:
events:
signing:
enabled: true
key-alias: event-signing-key
algorithm: ES256
When enabled, every event gets an ES256 (or configured algorithm) signature using the referenced KMS key. This provides non-repudiation, you can prove that a specific event was produced by a specific service at a specific time.
Command Integration
The event system integrates with the EDK's command lifecycle via a command extension. When enabled, every command execution automatically emits events for success and failure:
sphereon:
events:
command-extension:
enabled: true
include-patterns: ["party.**", "identity.**", "kms.**"]
exclude-patterns: ["health.**", "actuator.**"]
emit-on-success: true
emit-on-failure: true
This means you get an event stream of all command activity without modifying any command implementation. The events include the command ID, duration, result status, and session context.
Spring Boot Configuration
sphereon:
events:
enabled: true
store:
type: postgresql
retention-days: 90
retention-enabled: true
command-extension:
enabled: true
include-patterns: ["**"]
exclude-patterns: ["health.**", "actuator.**"]
hub:
buffer-capacity: 1000
signing:
enabled: false
The Spring auto-configuration registers the EventHub, persistent stores, retention job, and command extension as beans. The @ConditionalOnProperty guards ensure nothing is activated unless explicitly enabled.