A proof-of-concept demonstrating Event Sourcing patterns using Axon Framework with a clean architecture approach that isolates the framework from the domain core.
This project showcases how to implement event sourcing while maintaining a clear separation between:
- Domain/Core Logic - Framework-agnostic business logic
- Infrastructure - Axon Framework integration layer
The domain model (Wallet aggregate) remains completely free of Axon-specific annotations, making it portable and testable without framework dependencies.
┌─────────────────────────────────────────────────────────────────────────┐
│ REST API │
│ (WalletController) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER (infra/) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Axon Adapters (infra/axon/) │ │
│ │ ┌─────────────┐ ┌──────────────────────┐ ┌────────────────┐ │ │
│ │ │ Aggregate │ │ AggregateCommandHdlr │ │ AggregateEvent │ │ │
│ │ │ (Axon) │ │ │ │ SourcingHandler│ │ │
│ │ └──────┬──────┘ └──────────┬───────────┘ └───────┬────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────┴──────────────────────────────────────────┴────────┐ │ │
│ │ │ Event Processors (Subscribing/Tracking) │ │ │
│ │ │ AggregateSubscribingEventProcessor │ │ │
│ │ │ AggregateTrackingEventProcessor │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
────────────────┼────────────────
Interface Boundary
────────────────┼────────────────
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ CORE LAYER (core/) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Aggregate (core/aggregate/) │ │
│ │ Wallet (POJO) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Interfaces (core/command/, core/event/) │ │
│ │ WalletCommand, WalletEvent, WalletCommandHandler, │ │
│ │ WalletEventSourcingHandler, EventProcessor │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Feature Modules (core/modules/) │ │
│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │ │
│ │ │ walletcreate │ │ carddeposit │ │ cardwithdrawal │ │ │
│ │ │ - Command │ │ - Command │ │ - Command │ │ │
│ │ │ - CmdHandler │ │ - CmdHandler │ │ - CmdHandler │ │ │
│ │ │ - Event │ │ - Event │ │ - Event │ │ │
│ │ │ - EventSrcHdl │ │ - EventSrcHdl │ │ - EventSrcHdl │ │ │
│ │ │ - EventProc │ │ - EventProc │ │ - EventProc │ │ │
│ │ └────────────────┘ └────────────────┘ └────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
src/main/kotlin/com/example/eventsourcing/
├── EventSourcingWithAxonApplication.kt # Spring Boot entry point
│
├── core/ # 🎯 DOMAIN CORE (Framework-Agnostic)
│ ├── aggregate/
│ │ └── Wallet.kt # Plain domain entity (POJO)
│ │
│ ├── command/
│ │ ├── WalletCommand.kt # Base command abstraction
│ │ └── handler/
│ │ └── WalletCommandHandler.kt # Command handler interface
│ │
│ ├── event/
│ │ ├── WalletEvent.kt # Base event abstraction
│ │ ├── projection/
│ │ │ ├── EventProcessor.kt # Event processor interface
│ │ │ ├── SubscribingEventProcessor.kt # Subscribing processor marker
│ │ │ └── TrackingEventProcessor.kt # Tracking processor marker
│ │ └── replay/
│ │ └── WalletEventSourcingHandler.kt # Event sourcing handler interface
│ │
│ └── modules/ # 📦 FEATURE MODULES
│ ├── walletcreate/
│ │ ├── command/
│ │ │ ├── CreateWalletCommand.kt
│ │ │ └── CreateWalletCommandHandler.kt
│ │ └── event/
│ │ ├── WalletCreatedEvent.kt
│ │ ├── WalletCreatedEventProcessor.kt
│ │ └── WalletCreatedEventSourcingHandler.kt
│ │
│ ├── carddeposit/
│ │ ├── command/
│ │ │ ├── DepositCardCommand.kt
│ │ │ └── DepositCardCommandHandler.kt
│ │ └── event/
│ │ ├── CardDepositedEvent.kt
│ │ ├── CardDepositedEventProcessor.kt
│ │ └── CardDepositedEventSourcingHandler.kt
│ │
│ └── cardwithdrawal/
│ ├── command/
│ │ ├── WithdrawCardCommand.kt
│ │ └── WithdrawCardCommandHandler.kt
│ └── event/
│ ├── CardWithdrawnEvent.kt
│ ├── CardWithdrawnEventProcessor.kt
│ └── CardWithdrawnEventSourcingHandler.kt
│
└── infra/ # 🔧 INFRASTRUCTURE (Axon Framework)
├── axon/
│ ├── Aggregate.kt # Axon @Aggregate adapter
│ ├── AggregateCommand.kt # Axon CommandMessage adapter
│ ├── AggregateCommandHandler.kt # Dispatches to core handlers
│ ├── AggregateEventSourcingHandler.kt # Dispatches to core handlers
│ ├── AggregateSubscribingEventProcessor.kt # Subscribing event group
│ └── AggregateTrackingEventProcessor.kt # Tracking event group
│
├── config/
│ ├── AxonConfig.kt # Axon JPA event store config
│ └── JpaConfig.kt # JPA entity scanning
│
└── controller/
└── WalletController.kt # REST API endpoints
The core layer contains no Axon Framework annotations. All Axon-specific code resides in the infra/axon/ package:
| Core Layer (Framework-Free) | Infrastructure Layer (Axon) |
|---|---|
Wallet (plain class) |
Aggregate (@Aggregate) |
WalletCommand |
AggregateCommand (@TargetAggregateIdentifier) |
WalletCommandHandler |
AggregateCommandHandler (@CommandHandler) |
WalletEventSourcingHandler |
AggregateEventSourcingHandler (@EventSourcingHandler) |
EventProcessor |
AggregateSubscribingEventProcessor (@EventHandler) |
The infrastructure adapters use reflection-based dispatching to route commands/events to the appropriate core handlers:
// AggregateCommandHandler dynamically finds the right handler
override fun handle(command: WalletCommand, wallet: Wallet): WalletEvent {
val handler = commandHandlerMap[command.javaClass]
return handler.handle(command, wallet)
}Two event processing strategies are configured:
| Mode | Class | Purpose |
|---|---|---|
| Subscribing | AggregateSubscribingEventProcessor |
Synchronous, same-thread processing |
| Tracking | AggregateTrackingEventProcessor |
Asynchronous, persistent tracking token |
Configure in application.properties:
axon.eventhandling.processors.subscribing-event-group.mode=subscribing
axon.eventhandling.processors.tracking-event-group.mode=tracking1. Command Received
└─► WalletController receives HTTP request
2. Command Dispatched
└─► CommandGateway.send(CreateWalletCommand)
3. Axon Aggregate Handles
└─► Aggregate.handle(command) [@CommandHandler]
└─► AggregateCommandHandler.handle()
└─► CreateWalletCommandHandler.handle(command, wallet)
└─► Returns WalletCreatedEvent
4. Event Applied
└─► AggregateLifecycle.apply(event)
5. Event Sourcing (State Reconstruction)
└─► Aggregate.handle(event) [@EventSourcingHandler]
└─► AggregateEventSourcingHandler.handle()
└─► WalletCreatedEventSourcingHandler.handle(event, wallet)
└─► wallet.walletId = event.walletId
└─► wallet.balance = event.initialBalance
6. Event Stored
└─► JpaEventStorageEngine persists to database
7. Event Projected (Subscribing)
└─► AggregateSubscribingEventProcessor.process(event)
└─► WalletCreatedEventProcessor.process(event)
8. Event Projected (Tracking)
└─► AggregateTrackingEventProcessor.process(event)
└─► (Any TrackingEventProcessor implementations)
| Technology | Version | Purpose |
|---|---|---|
| Kotlin | 2.2.21 | Primary language |
| Spring Boot | 3.5.9 | Application framework |
| Axon Framework | 4.11.1 | Event Sourcing & CQRS |
| H2 Database | - | In-memory event store |
| JPA/Hibernate | - | Persistence |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/wallets |
Create a new wallet |
| POST | /api/wallets/{id}/deposit |
Deposit funds |
| POST | /api/wallets/{id}/withdraw |
Withdraw funds |
# Create wallet
curl -X POST http://localhost:8080/api/wallets \
-H "Content-Type: application/json" \
-d '{"walletId": "wallet-1", "initialBalance": 100}'
# Deposit
curl -X POST http://localhost:8080/api/wallets/wallet-1/deposit \
-H "Content-Type: application/json" \
-d '{"amount": 50}'
# Withdraw
curl -X POST http://localhost:8080/api/wallets/wallet-1/withdraw \
-H "Content-Type: application/json" \
-d '{"amount": 30}'# Build
./gradlew build
# Run
./gradlew bootRun
# Access H2 Console
# URL: http://localhost:8080/h2-console
# JDBC URL: jdbc:h2:mem:axondb- Testability - Core domain can be unit tested without Axon
- Portability - Easy to swap Axon for another framework
- Clean Separation - Infrastructure concerns don't leak into domain
- Modularity - Each feature is self-contained in its module
- Flexibility - Multiple event processing strategies supported