Skip to content

vovten/event-flow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

360 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Event Flow

License: Apache 2.0 Java

Event Flow is a lightweight Java framework for building event-driven applications. It provides the structural backbone for publishing, routing, and processing events β€” so you can focus on business logic instead of wiring infrastructure.

πŸ“– Table of Contents

✨ Features

  • Flexible Routing β€” Event channels with configurable transports
  • Multiple Transports β€” LocalQueue (in-JVM) and Apache Kafka out of the box, with extension points for custom transports
  • Annotation-Based β€” Event handling via @EventListener
  • Interface-Based β€” Event handling via EventSubscriber interface
  • POJO/Record Events β€” Support for plain Java objects without Event interface
  • Idempotency β€” Event deduplication based on UID
  • Transactional Publishing β€” Send events after transaction commit
  • Structured Logging β€” Decorators for publisher and dispatcher with machine-parseable JSON output
  • Retry Mechanism β€” Exponential backoff with configurable parameters
  • Lifecycle Tracking β€” End-to-end event lifecycle with persistent storage, status tracking, automatic retry of failed events, and acknowledgment-based monitoring
  • Extensible Serialization β€” JSON and MessagePack with support for custom formats

πŸ— Architecture

High-Level Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Event Flow Architecture                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Service    │────────▢│           Event Channels                β”‚   β”‚
β”‚  β”‚  (Publisher) β”‚         β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚  β”‚  Internal   β”‚  β”‚    External     β”‚   β”‚   β”‚
β”‚                           β”‚  β”‚  (in-JVM)   β”‚  β”‚    (Kafka)      β”‚   β”‚   β”‚
β”‚                           β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚                           β”‚         β”‚                  β”‚            β”‚   β”‚
β”‚                           └─────────┼───────────────── β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                     β”‚                  β”‚                β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ ▼────────────┐   β”‚
β”‚                           β”‚     Outgoing Event Transports           β”‚   β”‚
β”‚                           β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚                           β”‚  β”‚ LocalQueue  β”‚  β”‚      Kafka      β”‚   β”‚   β”‚
β”‚                           β”‚  β”‚   Queue     β”‚  β”‚    Producer     β”‚   β”‚   β”‚
β”‚                           β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
                                    β”‚  External Communication
                                    β”‚  (Network / Message Broker)
                                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    External Event Flow (Kafka)                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                         β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                           β”‚      Incoming Event Transports          β”‚   β”‚
β”‚                           β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚                           β”‚  β”‚ LocalQueue  β”‚  β”‚      Kafka      β”‚   β”‚   β”‚
β”‚                           β”‚  β”‚   Queue     β”‚  β”‚    Consumer     β”‚   β”‚   β”‚
β”‚                           β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                     β”‚                 β”‚                 β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚                           β”‚           Event Dispatcher             β”‚    β”‚
β”‚                           β”‚                                        β”‚    β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                     β”‚                 β”‚                 β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚                           β”‚           Handler Registry             β”‚    β”‚
β”‚                           β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚    β”‚
β”‚                           β”‚  β”‚ Annotation  β”‚  β”‚   Interface     β”‚  β”‚    β”‚
β”‚                           β”‚  β”‚   Based     β”‚  β”‚     Based       β”‚  β”‚    β”‚
β”‚                           β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚    β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                     β”‚                 β”‚                 β”‚
β”‚                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚                           β”‚        Event Handlers                  β”‚    β”‚
β”‚                           β”‚   (@EventListener / EventSubscriber)   β”‚    β”‚
β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Microservices Communication

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Microservice A (Event Producer)                        β”‚
β”‚                                                                            β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚   β”‚ Service  │──▢│ Publisher │──▢│ Channel  │──▢│ KafkaOutTransport    β”‚   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                             β”‚ produce()
                                                             β–Ό
               ═══════════════════════════════════════════════════
                    Apache Kafka β€” Topic: "events" 
               ═══════════════════════════════════════════════════
                ┃                                              ┃
     consume()  ┃                                   consume()  ┃
       β”‚        ┃                                     β”‚        ┃
       β–Ό        ┃                                     β–Ό        ┃
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        Microservice B (Consumer)    β”‚ β”‚        Microservice C (Consumer)    β”‚
β”‚                                     β”‚ β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚ β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚ KafkaInTransport β”‚               β”‚ β”‚    β”‚ KafkaInTransport β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚ β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚           β”‚                         β”‚ β”‚             β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚ β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚ EventDispatcher  β”‚               β”‚ β”‚    β”‚ EventDispatcher  β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚ β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚           β”‚ dispatch()              β”‚ β”‚             β”‚ dispatch()            β”‚
β”‚           β”‚                         β”‚ β”‚             β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚ β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚
β”‚  β”‚ HandlerRegistry  β”‚               β”‚ β”‚    β”‚ HandlerRegistry β”‚              β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚ β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚           β”‚ getHandlers()           β”‚ β”‚             β”‚ getHandlers()         β”‚
β”‚           β–Ό                         β”‚ β”‚             β–Ό                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚ β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚    Handler 1     β”‚               β”‚ β”‚    β”‚    Handler 2     β”‚             β”‚
β”‚  β”‚    Handler 2     β”‚               β”‚ β”‚    β”‚    Handler 3     β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚ β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Single Application Flow (Internal Events)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           Single Application                                β”‚
β”‚                                                                             β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚   β”‚ Service  │──▢│ Publisher │──▢│ Channel  │──▢│LocalQueueOutTransportβ”‚    β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                                            β”‚ offer(event)   β”‚
β”‚                                                            β–Ό                β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚              β”‚              BlockingDeque<Event>                     β”‚      β”‚
β”‚              β”‚                    (shared queue)                     β”‚      β”‚
β”‚              β””β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚                β”‚ take(event)                                                β”‚
β”‚                β–Ό                                                            β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚   β”‚ LocalQueueInTrans│──▢│ EventDispatcher  │──▢│ EventHandlerRegistry β”‚    β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                   β”‚ dispatch()             β”‚ getHandlers()  β”‚
β”‚                                   β”‚                        β–Ό                β”‚
β”‚                                   β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                                   └─────────────▢│     Handlers         β”‚   β”‚
β”‚                                    async execute β”‚  (virtual threads)   β”‚   β”‚
β”‚                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Modules

Module Description Documentation
event-flow-core Core module β€” framework-agnostic, pure Java 21+ README
event-flow-spring Spring Boot auto-configuration with YAML README

πŸ“¦ Installation

Maven

Add the dependency to your pom.xml:

<dependency>
    <groupId>io.github.vovten</groupId>
    <artifactId>event-flow</artifactId>
    <version>1.1.0</version>
</dependency>

For Spring Boot integration:

<dependency>
    <groupId>io.github.vovten</groupId>
    <artifactId>event-flow-spring</artifactId>
    <version>1.1.0</version>
</dependency>

Gradle

implementation 'io.github.vovten:event-flow:1.1.0'
// For Spring Boot:
implementation 'io.github.vovten:event-flow-spring:1.1.0'

Requirements

  • Java 21+
  • Apache Kafka 3.6.0+ (optional, for external events)

πŸš€ Quick Start

1. Create an Event

Events can be defined in two ways:

a) Use the @Event annotation (recommended) β€” cleaner POJO/record, channels from annotation:

@Event(channels = InternalEventChannel.class)
public record OrderShipped(String orderId, String customerId) {}

b) Implement the Event interface β€” full control over type and channels:

public record OrderCreatedEvent(String orderId, String customerId) implements Event {

    @Override
    public Class<?> type() {
        return OrderCreatedEvent.class;
    }
}

Both approaches work with publish(), prepare(), and handler registration. Recommended: @Event annotation for cleaner code; use implements Event only when you need full control.

2. Set Up Infrastructure

// Create local queue transport for in-JVM event delivery
var transports = new LocalQueueTransportsBuilder()
    .queueSize(1000)
    .build();

// Create a channel for internal (in-application) events
// For external events (Kafka), use ExternalEventChannel with Kafka transports
EventChannel internalChannel = new InternalEventChannel(
    List.of(transports.publisher())
);

// Create publisher (add externalChannel here for external events)
EventPublisher eventPublisher = EventPublisherBuilder.create(internalChannel)
    .retryable(3, Duration.ofMillis(100), 2.0)
    .buildAndLog();

// Create handler registry
EventHandlerRegistry handlerRegistry = EventHandlerRegistryBuilder.create()
    .withAnnotationListeners()
    .withInterfaceListeners()
    .buildAndLog();

// Create dispatcher
EventDispatcher eventDispatcher = EventDispatcherBuilder.create()
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .handlerRegistry(handlerRegistry)
    .transports(List.of(transports.dispatcher()))
    .concurrencyLimit(100)
    .buildAndLog();

// Start the dispatcher
eventDispatcher.start(eventDispatcher::dispatch);

3. Create a Handler (Annotation-Based)

Handle the event directly:

public class OrderEventHandler {

    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println("Order created: " + event.orderId());
    }
}

// Register the handler
handlerRegistry.register(new OrderEventHandler());

Or receive the full Envelope with metadata (eventId, processId, occurredAt, etc.):

public class OrderEnvelopeHandler {

    @EventListener(OrderShipped.class)
    public void handleOrderShipped(Envelope<OrderShipped> envelope) {
        OrderShipped event = envelope.payload();
        System.out.println("Order " + event.orderId()
                + " processed with id " + envelope.eventId());
    }
}

// Register the envelope handler
handlerRegistry.register(new OrderEnvelopeHandler());

When the handler parameter is Envelope<T>, the @EventListener annotation must specify the payload type explicitly (e.g., @EventListener(OrderShipped.class)). This is especially useful for POJO/record events annotated with @Event β€” they are automatically wrapped in an Envelope upon publishing.

4. Publish an Event

public class OrderService {

    private final EventPublisher eventPublisher;

    public OrderService(EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void createOrder(String customerId) {
        // Business logic...
        eventPublisher.publish(new OrderCreatedEvent("order-123", customerId));
    }
}

πŸ”§ Core Components

Event

Base interface for all events. Defines event type and publication channels.

public interface Event {
    Class<?> type();
    default List<Class<? extends EventChannel>> channels() {
        return List.of(InternalEventChannel.class);
    }
    default String asJson() {
        return EventUtils.toJson(this);
    }
}

TraceableEvent β€” extends Event with tracing fields: eventId (UUID), processId (correlation), occurredAt (timestamp).

Envelope

Wrapper for domain events that adds technical metadata. Automatically captures:

  • eventId (UUID) β€” unique event identifier
  • processId (UUID) β€” correlation ID (e.g., saga ID)
  • occurredAt (Instant) β€” event timestamp
  • metadata (Map) β€” custom key-value pairs
  • payload β€” the actual domain object

The envelope implements Event interface, so it passes through existing transport infrastructure.

Creating Envelopes:

// Auto-generated metadata
Envelope<OrderCreatedEvent> envelope = Envelope.of(new OrderCreatedEvent("123"));

// With custom processId (correlation)
UUID processId = UUID.fromString("...");
Envelope<OrderCreatedEvent> envelope = Envelope.of(new OrderCreatedEvent("123"), processId);

// With explicit channels
Envelope<OrderCreatedEvent> envelope = Envelope.of(
    new OrderCreatedEvent("123"),
    List.of(ExternalEventChannel.class)
);

Channels from @Event Annotation: POJO/record classes can use the @Event annotation to specify default channels:

@Event(channels = ExternalEventChannel.class)
public record OrderCreatedEvent(String orderId) {}

EventChannel

A channel defines event delivery routes through transports.

public interface EventChannel {
    String name();
    List<OutTransport> transports();
    CompletableFuture<SendResults> send(Event event);
}

Built-in Channels:

  • InternalEventChannel β€” for internal in-application delivery
  • ExternalEventChannel β€” for external delivery to other applications/microservices

EventPublisher

Publishes events to configured channels.

Creating via Builder:

EventPublisher publisher = EventPublisherBuilder.create(internalChannel, externalChannel)
    .retryable(3, Duration.ofMillis(100), 2.0)
    .build();

EventPublisherBuilder β€” fluent builder for creating publishers with flexible configuration:

Method Description
create(...) Create builder with event channels (required)
retryable() Enable retry with default settings (3 attempts, 100ms initial delay, 2.0 multiplier)
retryable(max, delay, multiplier) Enable retry with custom settings
withDecorator(fn) Add custom decorator to the publisher chain
build() Build the publisher
buildAndLog() Build the publisher and log the configuration
loggable() Enable structured logging (JSON, 1024 char payload limit)
loggable(maxPayloadLength) Enable structured logging with custom payload truncation
loggable(maxPayloadLength, excludedEvents) Enable structured logging with event type exclusion
loggable(maxPayloadLength, excludedEvents, logLevels) Enable structured logging with per-event log level overrides

EventDispatcher

Delivers events from transports to handlers.

public interface EventDispatcher {
    void dispatch(Event event);
    void register(Object listener);
    boolean isRegistered(Object listener);
    void start(Consumer<Event> handler);
    void stop();
}

EventDispatcherBuilder β€” fluent builder for creating dispatchers:

Method Description
executor(...) Configure ExecutorService (required)
handlerRegistry(...) Handler registry (required)
transports(...) List of incoming transports
concurrencyLimit(n) Concurrency limiting via Semaphore
idempotent() Enable idempotency (deduplication by UID)
idempotent(maxSize, ttl) Enable idempotency with custom settings
withDecorator(fn) Add custom decorator
build() Build the dispatcher
buildAndLog() Build the dispatcher and log the configuration
loggable() Enable structured logging (JSON, 1024 char payload limit)
loggable(maxPayloadLength) Enable structured logging with custom payload truncation
loggable(maxPayloadLength, excludedEvents) Enable structured logging with event type exclusion
loggable(maxPayloadLength, excludedEvents, logLevels) Enable structured logging with per-event log level overrides

EventHandlerRegistry

Registry for discovering and managing event handlers.

public interface EventHandlerRegistry {
    List<EventHandler> getHandlers(Event event);
    void register(Object listener);
    void unregister(Object listener);
    boolean isRegistered(Object listener);
    void merge(EventHandlerRegistry registry);
    int handlerCount();
    String name();
}

Built-in implementations:

  • EventListenerRegistry β€” discovery via @EventListener annotation
  • EventSubscriberRegistry β€” discovery via EventSubscriber interface
  • CompositeEventHandlerRegistry β€” combines multiple registries

EventHandlerRegistryBuilder β€” fluent builder for creating registries:

Method Description
withAnnotationListeners() Enable discovery via @EventListener annotation
withInterfaceListeners() Enable discovery via EventSubscriber interface
withCustomRegistry(registry) Add a custom registry
withDecorator(fn) Add a decorator
build() Build the registry
buildAndLog() Build the registry and log the configuration

EventLifecycle

An enum that controls how an event's journey is tracked. Three levels:

Level Behaviour
NONE Fire-and-forget β€” event passes through without any persistence
PERSISTED Event is stored in the EventStore (with UNDEFINED status) but not actively tracked
MANAGED Full lifecycle tracking β€” event status transitions through NEW β†’ PUBLISHED β†’ HANDLED, with automatic retry on failure

Set via @Event(lifecycle = ...) annotation or the lifecycle() default method on Event.

EventStore

Persistence layer for lifecycle tracking. Stores serialised events and tracks their status as they flow through the system.

  • JdbcEventStore β€” production-grade, backed by a relational database (PostgreSQL, H2, MySQL, etc.)
  • InMemoryEventStore β€” in-JVM ConcurrentHashMap-backed store for testing and single-JVM scenarios

EventLifecyclePublisher

Decorator for EventPublisher that persists events to the EventStore before they are sent and updates their status (NEW β†’ PUBLISHED/FAILED) after publishing. See the Lifecycle Tracking section.

EventLifecycleDispatcher

Decorator for EventDispatcher that publishes SuccessAck or FailureAck events back to the source channels after handler execution β€” enabling end-to-end status tracking. See the Lifecycle Tracking section.

EventRetryScheduler

Periodically scans the EventStore for failed (FAILED), stuck (PUBLISHED), and orphaned (NEW) events and re-publishes them with exponential backoff. See the Lifecycle Tracking section.

AckHandler

An EventSubscriber that processes incoming lifecycle acknowledgment events (SuccessAck/FailureAck) and updates the event status in the EventStore. Filters by service name to allow multiple publisher instances to share the same channel. See the Lifecycle Tracking section.

EventTransport

Transports for event delivery.

Incoming Transports (InTransport):

  • LocalQueueInTransport β€” receive from local queue
  • KafkaInTransport β€” receive from Kafka topics

Outgoing Transports (OutTransport):

  • LocalQueueOutTransport β€” send to local queue
  • KafkaOutTransport β€” send to Kafka topic
  • BroadcastKafkaOutTransport β€” send to all Kafka topic partitions

LocalQueueTransportsBuilder

Utility for creating paired incoming/outgoing transports based on a local queue:

var transports = new LocalQueueTransportsBuilder()
    .queueSize(1000)
    .build();

EventChannel channel = new InternalEventChannel(
    List.of(transports.publisher())
);

EventDispatcher dispatcher = EventDispatcherBuilder.create()
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .handlerRegistry(handlerRegistry)
    .transports(List.of(transports.dispatcher()))
    .build();

dispatcher.start(dispatcher::dispatch);

Serialization

EventSerializer β€” serialization interface with magic byte prefix:

  • 0x01 β€” JSON
  • 0x02 β€” MessagePack

EventSerializerFactory β€” factory with automatic format detection:

// Register a custom serializer
EventSerializerFactory.getInstance().register(new MyCustomSerializer());

EventTypeRegistry β€” security whitelist for allowed event classes:

// Allow a package (default: io.github.vovten.eventflow.*)
EventTypeRegistry.allowPackage("com.example.events");

// Allow a specific class
EventTypeRegistry.allowClass(MyEvent.class);

πŸ”„ Lifecycle Tracking

Event Flow provides an end-to-end lifecycle tracking system that answers three questions about every event:

  • Was it saved? β€” the event persisted before publication (crash recovery)
  • Was it delivered? β€” the event reached all target channels successfully
  • Was it handled? β€” all registered handlers processed the event without errors

This turns event publishing from a fire-and-forget operation into a reliable, observable process β€” essential for critical business events (orders, payments, notifications) where you need guarantees and visibility.

When you need it

Scenario Without lifecycle With lifecycle (MANAGED)
Service crashes mid-publish Event lost Event safe in store, retried on restart
Handler throws an exception Silent failure Status updated to FAILED, automatic retry
Ack lost in transit Event stuck in limbo Detected as PUBLISHED β†’ retried
Debugging production issues Logs only Queryable event store with full history

You choose the level per event β€” NONE for high-throughput fire-and-forget, PERSISTED for audit without monitoring, MANAGED when you need guarantees.

How it works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ PUBLISHER ────────────────────────────┐
β”‚                                                                  β”‚
β”‚  Event ──► EventLifecyclePublisher ──► EventStore ──► Channel    β”‚
β”‚                    β”‚                         β”‚                   β”‚
β”‚                    β”‚  saves & tracks status  β”‚                   β”‚
│                    │  (NEW→PUBLISHED/FAILED) │                   │
β”‚                    β”‚                         β”‚                   β”‚
β”‚  ◄── EventRetryScheduler scans FAILED, PUBLISHED, NEW ──► retry  β”‚
β”‚                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                                  β–²
         β”‚         SuccessAck / FailureAck  β”‚
         β–Ό                                  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ DISPATCHER ────────────────────────────┐
β”‚                                                                  β”‚
β”‚  Channel ──► EventLifecycleDispatcher ──► Handlers               β”‚
β”‚                      β”‚                                           β”‚
β”‚                      β”‚  publishes ack after handler execution    β”‚
β”‚                                                                  β”‚
β”‚  ◄── AckHandler processes ack, updates store (HANDLED/FAILED)    β”‚
β”‚                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The flow:

  1. EventLifecyclePublisher saves the event to EventStore with status NEW, then publishes it
  2. On successful delivery, status updates to PUBLISHED; on failure, to FAILED
  3. EventLifecycleDispatcher (on the consumer side) publishes SuccessAck or FailureAck back through the channel
  4. AckHandler (on the publisher side) catches the ack and updates status to HANDLED or FAILED
  5. EventRetryScheduler periodically rescans FAILED, stuck PUBLISHED, and orphaned NEW events and retries them with exponential backoff

Three lifecycle levels

Level Behaviour Use case
NONE No persistence, no tracking. Event passes straight through High-throughput notifications, ephemeral events
PERSISTED Event saved to store but status stays UNDEFINED. No retry, no ack Audit trail without operational guarantees
MANAGED Full tracking: NEW β†’ PUBLISHED β†’ HANDLED, automatic retry on failure, ack-based end-to-end confirmation Orders, payments, critical business events

Set via @Event annotation:

@Event(channels = InternalEventChannel.class, lifecycle = EventLifecycle.MANAGED)
public record OrderCreated(String orderId) {}

Or via Event.lifecycle() default method β€” the annotation takes precedence when both are present.

Configuration

Enable lifecycle tracking in Spring Boot with three properties:

event-flow:
  publisher:
    lifecycle:
      enabled: true              # Enable lifecycle-aware publishing
      service-name: order-service # Required! Identifies this service for ack filtering
      store:
        type: db                 # "db" (PostgreSQL/H2/MySQL) or "in-memory" (testing)
        table-name: event_store
        auto-init-schema: true    # Disable in production, manage DDL via Flyway
      retry:
        enabled: true
        max-retries: 3
        retry-interval: 30s
        min-age: 30s              # Backoff: 30s, then 60s, then 120s
  dispatcher:
    lifecycle:
      enabled: true               # Enable ack generation on the dispatcher side

The service-name is mandatory β€” it identifies this service instance so that AckHandler only processes acknowledgments meant for it (multiple services can share the same ack channel without interference).

Retry mechanism

EventRetryScheduler scans for three categories of events that need retry:

Status Why it happens
FAILED Publication or handler threw an error
PUBLISHED Event was sent but ack was lost in transit (stuck)
NEW Event was saved but the application crashed before publishing finished (orphaned)

Backoff is exponential: delay = minAge Γ— 2^retryCount. First retry at 30s, second at 60s, third at 120s.

Cleanup

EventCleanupScheduler periodically deletes old terminal events (HANDLED and UNDEFINED) from the EventStore to prevent unbounded growth. Deletion is performed in configurable batches with pauses between batches to reduce database load.

event-flow:
  publisher:
    lifecycle:
      cleanup:
        enabled: true
        max-age: 7d           # Events older than this are deleted
        batch-size: 500       # Rows per DELETE
        interval: 60m         # How often the scheduler runs
        pause-between-batches: 100ms  # Throttle between batches

Safety:

  • Only terminal statuses (HANDLED, UNDEFINED) are cleaned up β€” FAILED events are preserved for manual inspection
  • A single cycle deletes at most 100 000 events; leftover events are picked up by the next cycle
  • A random jitter (up to interval) is added to the first run to avoid thundering herd when multiple instances start

Storage

The EventStore interface has two built-in implementations:

  • JdbcEventStore β€” production grade, backed by a relational database. Table is created automatically by default. Dialect-specific DDL scripts are shipped at io/github/vovten/eventflow/lifecycle/store/db/event-store-<dialect>.sql for manual migration tooling.
  • InMemoryEventStore β€” ConcurrentHashMap-backed, for testing and single-JVM scenarios where persistence is not needed.

You can also implement EventStore with your own backend (Redis, MongoDB, etc.) and configure it via store.type.


πŸ“ Usage Examples

Publishing Options Overview

Event Flow provides multiple ways to publish events:

Method Envelope Channels Metadata Message Size
implements Event ❌ No From event Minimal ⚑ Smallest
prepare().publish() βœ… Auto Custom Custom Medium

Note: When POJO/record or prepare() is used, an Envelope is automatically created wrapping the payload with additional metadata (eventId, processId, occurredAt). This increases message size but adds correlation/tracing capabilities.

Recommended: @Event Annotation + prepare() Builder

The most convenient approach for most use cases:

// Define event with default channels via annotation
@Event(channels = {InternalEventChannel.class, ExternalEventChannel.class})
public record OrderCreatedEvent(long orderId) {}

// Publish with custom metadata (channels from annotation are used automatically)
eventPublisher.prepare(new OrderCreatedEvent(1))
    .withProcessId(processId)
    .publish();

This gives you:

  • Channels from @Event annotation (no need to specify in code)
  • Auto-generated eventId and occurredAt (timestamps)
  • Custom metadata via builder (processId, etc.)
  • Envelope for correlation/tracing

1. Fastest: Event Interface (No Envelope)

Implement Event interface for minimum overhead β€” no Envelope wrapper, smallest message size:

public record OrderCreatedEvent(String orderId, String email) implements Event {

    @Override
    public Class<?> type() {
        return OrderCreatedEvent.class;
    }

    @Override
    public List<Class<? extends EventChannel>> channels() {
        return List.of(InternalEventChannel.class, ExternalEventChannel.class);
    }
}

eventPublisher.publish(new OrderCreatedEvent("order-123", "user@example.com"));

Use cases: High-throughput scenarios, microservice-to-microservice communication, Kafka topics.

2. POJO/Record Publishing (Enveloped)

Publish any Java object directly β€” Envelope is created automatically:

// Simple POJO with @Event annotation
@Event(channels = InternalEventChannel.class)
public record OrderCreated(String orderId, String email) {}

eventPublisher.publish(new OrderCreated("order-123", "user@example.com"));

Envelope with auto-generated metadata:

  • eventId β€” random UUID
  • processId β€” null
  • occurredAt β€” current timestamp
  • metadata β€” empty
  • Channels β€” InternalEventChannel (default)

3. Full Control with prepare() Builder (Enveloped)

Use the builder for custom metadata and channels β€” same Envelope is created internally:

eventPublisher.prepare(new OrderCreated("order-123", "user@example.com"))
    .withMetadata("key1", "data1")
    .withMetadata("key2", "data2")
    .withChannels(InternalEventChannel.class, ExternalEventChannel.class)
    .withProcessId(UUID.randomUUID())
    .withOccurredAt(Instant.now())
    .publish();

Note: Channels specified via withChannels() have priority over channels defined in @Event annotation on the payload class.

Available builder methods:

  • withMetadata(key, value) β€” add single metadata entry
  • withMetadata(Map) β€” add multiple metadata entries
  • withChannel(channel) β€” set single channel (convenience alias)
  • withChannels(c1) β€” set one channel
  • withChannels(c1, c2) β€” set two channels
  • withChannels(c1, c2, c3) β€” set three channels
  • withChannels(List) β€” set arbitrary number of channels
  • withProcessId(UUID) β€” correlation ID (e.g., saga ID)
  • withOccurredAt(Instant) β€” custom event timestamp
  • publish() β€” send the event

4. POJO/Record with @Event Annotation

Specify default channels on the POJO/record class:

@Event(channels = ExternalEventChannel.class)
public record OrderShipped(String orderId, Instant shippedAt) {}

@Event(channels = {InternalEventChannel.class, ExternalEventChannel.class})
public record OrderDelivered(String orderId, Instant deliveredAt) {}

eventPublisher.publish(new OrderShipped("order-123", Instant.now()));

Comparison Table

Approach Best For Envelope Message Size
implements Event High throughput, Kafka, microservices ❌ Smallest
publish(POJO/record) Simple notifications, internal events βœ… Medium
publish(POJO/record) + @Event Default routing configuration βœ… Medium
prepare().publish() Custom metadata, dynamic routing βœ… Medium

Event with Multiple Channels

public record UserRegisteredEvent(String userId, String email) implements Event {

    @Override
    public Class<?> type() {
        return UserRegisteredEvent.class;
    }

    @Override
    public List<Class<? extends EventChannel>> channels() {
        return List.of(InternalEventChannel.class, ExternalEventChannel.class);
    }
}

Interface-Based Handler

public class NotificationEventSubscriber implements EventSubscriber {

    @Override
    public List<Class<? extends Event>> events() {
        return List.of(UserRegisteredEvent.class);
    }

    @Override
    public void onEvent(Event event) {
        if (event instanceof UserRegisteredEvent e) {
            sendWelcomeEmail(e.email());
        }
    }

    private void sendWelcomeEmail(String email) {
        // Email sending logic
    }
}

// Register the handler
handlerRegistry.register(new NotificationEventSubscriber());

Handling Envelope (entire wrapper with metadata)

Handlers can receive the entire Envelope including metadata:

public class OrderEventHandler {

    @EventListener
    public void handleOrder(Envelope<OrderPlacedEvent> envelope) {
        // Access payload
        OrderPlacedEvent event = envelope.payload();

        // Access metadata
        UUID eventId = envelope.eventId();
        UUID processId = envelope.processId();
        Instant occurredAt = envelope.occurredAt();
        Map<String, String> metadata = envelope.metadata();

        System.out.println("Processed: " + event.orderId());
    }
}

Note: When using Envelope as a handler parameter, you must specify the payload type in the annotation:

@EventListener(OrderPlacedEvent.class)
public void handleOrder(Envelope<OrderPlacedEvent> envelope) {
    // Handle envelope
}

Kafka Transport Configuration

// Outgoing transport
OutTransport kafkaOut = new KafkaOutTransport(
    "localhost:9092",  // bootstrap servers
    "events"           // topic
);

EventChannel externalChannel = new ExternalEventChannel(
    List.of(kafkaOut)
);

// Incoming transport
InTransport kafkaIn = new KafkaInTransport(
    "localhost:9092",  // bootstrap servers
    "events",          // topics (comma-separated)
    "event-dispatcher" // group.id
);

// Dispatcher with Kafka transport
EventDispatcher dispatcher = EventDispatcherBuilder.create()
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .handlerRegistry(handlerRegistry)
    .transports(List.of(kafkaIn))
    .build();

dispatcher.start(dispatcher::dispatch);

Idempotent Dispatcher

EventDispatcher idempotentDispatcher = EventDispatcherBuilder.create()
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .handlerRegistry(handlerRegistry)
    .transports(List.of(inTransport))
    .idempotent(10000, Duration.ofMinutes(5))  // max 10000 entries, 5 min TTL
    .build();

Transactional Publishing

For transactional publishing in Spring Boot applications, see event-flow-spring/README.md.

βš™οΈ Configuration

For detailed configuration examples, see:

  • Event Flow Core β€” LocalQueue, Kafka, custom transports, serialization
  • Event Flow Spring β€” YAML auto-configuration, transactional publishing, retry support, lifecycle tracking

Lifecycle Tracking Configuration

Property Default Description
event-flow.publisher.lifecycle.enabled false Enable lifecycle-aware event publishing
event-flow.publisher.lifecycle.service-name "" Required! Service name for ack filtering
event-flow.publisher.lifecycle.store.type db Store type: db, in-memory, or custom
event-flow.publisher.lifecycle.store.table-name event_store Custom table name (for db type)
event-flow.publisher.lifecycle.store.auto-init-schema true Auto-create table on startup
event-flow.publisher.lifecycle.retry.enabled true Enable automatic retry of failed events
event-flow.publisher.lifecycle.retry.max-retries 3 Maximum retry attempts
event-flow.publisher.lifecycle.retry.retry-interval 30s Interval between retry cycles
event-flow.publisher.lifecycle.retry.min-age 30s Base backoff for exponential retry
event-flow.dispatcher.lifecycle.enabled false Enable ack-based lifecycle tracking on dispatcher

LocalQueue

LocalQueue is a built-in in-JVM transport for internal event exchange. See event-flow-core/README.md for details.

Kafka

Kafka transport for external event communication. See event-flow-core/README.md for configuration examples.

Custom Transport

Implement OutTransport or InTransport interfaces to add custom transport types. See event-flow-core/README.md for an example.

πŸ”Š Structured Logging

Event Flow provides structured JSON logging decorators for both the publisher and the dispatcher. Each log entry captures: event status, envelope metadata (eventId, processId, occurredAt), payload (truncated), handler/transport results, duration, and distributed tracing context (traceId, spanId, deliveredFrom).

Enabling Logging

Use buildAndLog() or the loggable() builder methods:

// Publisher
EventPublisher publisher = EventPublisherBuilder.create(channel)
    .loggable()                                    // defaults: 1024 char payload
    .loggable(500)                                 // custom payload truncation
    .loggable(500, Set.of("HeartbeatEvent"))       // exclude noisy events
    .loggable(500, Set.of(), Map.of("HeartbeatEvent", "ERROR"))  // with log overrides
    .build();

// Dispatcher
EventDispatcher dispatcher = EventDispatcherBuilder.create()
    .executor(executor)
    .handlerRegistry(registry)
    .loggable()
    .build();

Per-Event Log Level Overrides

By default, log level is determined by the outcome:

Outcome Default level
All handlers/transports succeed INFO
Partial success (some fail) WARN
All fail or exception ERROR

With logLevels you can override the minimum log level for specific event types. The override acts as a threshold:

Override ERROR outcome WARN outcome INFO outcome
ERROR log.error suppressed suppressed
WARN log.error log.warn suppressed
INFO log.error log.warn log.info

Example: Suppress logging for a high-frequency heartbeat event, only show errors:

event-flow:
  dispatcher:
    logging:
      enabled: true
      log-levels:
        HeartbeatEvent: ERROR
        HealthCheckEvent: WARN

In Spring Boot, these settings go into event-flow.yml or application.yml. When using the builder directly (without Spring), pass the map via loggable(maxPayloadLength, excludedEvents, logLevels):

Map<String, String> logLevels = Map.of(
    "HeartbeatEvent", "ERROR",
    "HealthCheckEvent", "WARN"
);
EventDispatcher dispatcher = EventDispatcherBuilder.create()
    .executor(executor)
    .handlerRegistry(registry)
    .loggable(1024, Set.of(), logLevels)
    .build();

Valid level names: TRACE, DEBUG, INFO, WARN, ERROR.

πŸ“Š Interaction Diagrams

1. Event Publishing (Internal Channel)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Service β”‚   β”‚EventPublisherβ”‚   β”‚EventChannelβ”‚  β”‚LocalQueue β”‚   β”‚Dispatcherβ”‚  β”‚HandlerRegistryβ”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚               β”‚                 β”‚               β”‚              β”‚                β”‚
     β”‚ publish(event)β”‚                 β”‚               β”‚              β”‚                β”‚
     │──────────────▢│                 β”‚               β”‚              β”‚                β”‚
     β”‚               β”‚ send(event)     β”‚               β”‚              β”‚                β”‚
     β”‚               │────────────────▢│               β”‚              β”‚                β”‚
     β”‚               β”‚                 β”‚ offer(event)  β”‚              β”‚                β”‚
     β”‚               β”‚                 │──────────────▢│              β”‚                β”‚
     β”‚               β”‚                 β”‚               β”‚              β”‚                β”‚
     β”‚               β”‚                 β”‚               β”‚ take(event)  β”‚                β”‚
     β”‚               β”‚                 β”‚               │─────────────▢│                β”‚
     β”‚               β”‚                 β”‚               β”‚              β”‚                β”‚
     β”‚               β”‚                 β”‚               β”‚              β”‚getHandlers(event)
     β”‚               β”‚                 β”‚               β”‚              │───────────────▢│
     β”‚               β”‚                 β”‚               β”‚              │◀───────────────│
     β”‚               β”‚                 β”‚               β”‚              β”‚ [handlers]     β”‚
     β”‚               β”‚                 β”‚               β”‚              β”‚                β”‚
     β”‚               β”‚                 β”‚               β”‚              β”‚ async execute  β”‚
     β”‚               β”‚                 β”‚               β”‚              │───────────────▢│ onEvent(event)
     β”‚               β”‚                 β”‚               β”‚              β”‚                β”‚
     │◀──────────────│ (CompletableFuture)             β”‚              β”‚                β”‚

2. Event Publishing (External Channel via Kafka)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Service β”‚   β”‚EventPublisherβ”‚   β”‚KafkaOutTranspβ”‚   β”‚  Kafka   β”‚        β”‚KafkaInTransp β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚               β”‚                  β”‚                β”‚                     β”‚
     β”‚ publish(event)β”‚                  β”‚                β”‚                     β”‚
     │──────────────▢│                  β”‚                β”‚                     β”‚
     β”‚               β”‚ send(event)      β”‚                β”‚                     β”‚
     β”‚               │─────────────────▢│                β”‚                     β”‚
     β”‚               β”‚                  β”‚ produce(event) β”‚                     β”‚
     β”‚               β”‚                  │───────────────▢│                     β”‚
     β”‚               β”‚                  β”‚                β”‚                     β”‚
     β”‚               β”‚                  β”‚    ack(offset) β”‚                     β”‚
     β”‚               β”‚                  │◀───────────────│                     β”‚
     β”‚               │◀─────────────────│                β”‚                     β”‚
     β”‚               β”‚  SendResult      β”‚                β”‚                     β”‚
     │◀──────────────│                  β”‚                β”‚                     β”‚
     β”‚               β”‚                  β”‚                β”‚                     β”‚
     β”‚               β”‚                  β”‚                β”‚ poll(event)         β”‚
     β”‚               β”‚                  β”‚                │────────────────────▢│
     β”‚               β”‚                  β”‚                β”‚                     β”‚
     β”‚               β”‚                  β”‚                β”‚       event         β”‚
     β”‚               β”‚                  β”‚                │◀────────────────────│

3. Event Lifecycle Tracking (MANAGED)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Service  β”‚  β”‚EventLifecycle     β”‚  β”‚  EventStore  β”‚  β”‚   EventChannel β”‚  β”‚Lifecycle β”‚  β”‚AckHandlerβ”‚
β”‚          β”‚  β”‚   Publisher       β”‚  β”‚  (persistent)β”‚  β”‚   + Dispatcher β”‚  β”‚Dispatcherβ”‚  β”‚         β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚ publish(event)  β”‚                    β”‚                  β”‚                β”‚             β”‚
     │────────────────▢│                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚ save(NEW)          β”‚                  β”‚                β”‚             β”‚
     β”‚                 │───────────────────▢│                  β”‚                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚ send(event)        β”‚                  β”‚                β”‚             β”‚
     β”‚                 │──────────────────────────────────────▢│                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚ dispatch(event)β”‚             β”‚
     β”‚                 β”‚                    β”‚                  │───────────────▢│             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚ handlers    β”‚
     β”‚                 β”‚                    β”‚                  β”‚                │─────────▢   β”‚
     β”‚                 β”‚                    β”‚                  β”‚                │◀────────    β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚ results     β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚ update(PUBLISHED)  β”‚                  β”‚                β”‚             β”‚
     β”‚                 │◀───────────────────│                  β”‚                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚ publish ack β”‚
     β”‚                 β”‚                    β”‚                  β”‚                │─────────────│──────────▢
     β”‚                 β”‚                    β”‚                  β”‚                β”‚  (Success/  β”‚           β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚   Failure)  β”‚           β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚           β”‚
     β”‚                 β”‚                    β”‚                  β”‚                β”‚             β”‚ ack event β”‚
     β”‚                 β”‚                    β”‚ update(HANDLED/  β”‚                β”‚             │◀──────────│
     β”‚                 β”‚                    β”‚   FAILED)        β”‚                β”‚             β”‚           β”‚
     β”‚                 │◀───────────────────│                  β”‚                β”‚             β”‚           β”‚
     │◀────────────────│ (CompletableFuture)β”‚                  β”‚                β”‚             β”‚           β”‚

4. Event Dispatch to Multiple Handlers

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Dispatcher  β”‚   β”‚HandlerRegistryβ”‚   β”‚          Event Handlers             β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                   β”‚                          β”‚
       β”‚ dispatch(event)   β”‚                          β”‚
       │──────────────────▢│                          β”‚
       β”‚                   β”‚                          β”‚
       β”‚                   β”‚ getHandlers(event)       β”‚
       β”‚                   │─────────────────────────▢│
       β”‚                   β”‚                          β”‚
       β”‚                   β”‚ [Handler1, Handler2, ...]β”‚
       β”‚                   │◀─────────────────────────│
       β”‚                   β”‚                          β”‚
       β”‚ handlers          β”‚                          β”‚
       │◀──────────────────│                          β”‚
       β”‚                   β”‚                          β”‚
       β”‚ executor.execute(Handler1)                   β”‚
       │─────────────────────────────────────────────▢│ Handler1.onEvent()
       β”‚                   β”‚                          β”‚
       β”‚ executor.execute(Handler2)                   β”‚
       │─────────────────────────────────────────────▢│ Handler2.onEvent()
       β”‚                   β”‚                          β”‚
       β”‚       ...         β”‚                          β”‚
       β”‚                   β”‚                          β”‚

πŸ“š Documentation

🀝 Contributing

  1. Fork the repository
  2. Create a branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the Apache License 2.0 β€” see the LICENSE file for details.

πŸ‘₯ Authors

About

Event Flow is a lightweight Java framework for building event-driven applications.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages