Skip to main content
Version: v0.25.0 (Latest)

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

EventSubsystemWhen it fires
PARTY_CREATED / UPDATED / DELETEDPARTYParty lifecycle changes
IDENTITY_CREATED / UPDATED / DELETEDIDENTITYIdentity lifecycle changes
IDENTITY_VERIFIEDIDENTITYIdentity verification completed
CONTACT_CREATED / UPDATED / DELETEDCONTACTContact information changes

Tenant & Configuration

EventSubsystemWhen it fires
TENANT_CREATED / UPDATED / DELETEDTENANTTenant lifecycle changes
TENANT_SUSPENDED / TENANT_ACTIVATEDTENANTTenant state transitions
SETTINGS_CREATED / UPDATED / DELETEDSETTINGSConfiguration changes

Authorization & Security

EventSubsystemWhen it fires
AUTHZ_GRANTEDAUTHZPolicy evaluation returned PERMIT
AUTHZ_DENIEDAUTHZPolicy evaluation returned DENY
AUTHZ_ERRORAUTHZPolicy evaluation failed (PDP unreachable, etc.)

Infrastructure

EventSubsystemWhen it fires
DB_CONNECTION_ACQUIRED / RELEASED / FAILEDDB_ROUTINGDatabase 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:

Transmission 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.