Skip to content

ksevindik/event-sourcing-with-axon

Repository files navigation

Event Sourcing with Axon Framework - POC

A proof-of-concept demonstrating Event Sourcing patterns using Axon Framework with a clean architecture approach that isolates the framework from the domain core.

Overview

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.


High-Level Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                              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      │    │   │
│  │  └────────────────┘ └────────────────┘ └────────────────────┘    │   │
│  └──────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

Project Structure

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

Key Design Principles

1. Framework Isolation

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)

2. Dynamic Handler Dispatching

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)
}

3. Event Processing Modes

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=tracking

Event Sourcing Flow

1. 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 Stack

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

API Endpoints

Method Endpoint Description
POST /api/wallets Create a new wallet
POST /api/wallets/{id}/deposit Deposit funds
POST /api/wallets/{id}/withdraw Withdraw funds

Example Requests

# 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}'

Running the Application

# Build
./gradlew build

# Run
./gradlew bootRun

# Access H2 Console
# URL: http://localhost:8080/h2-console
# JDBC URL: jdbc:h2:mem:axondb

Benefits of This Architecture

  1. Testability - Core domain can be unit tested without Axon
  2. Portability - Easy to swap Axon for another framework
  3. Clean Separation - Infrastructure concerns don't leak into domain
  4. Modularity - Each feature is self-contained in its module
  5. Flexibility - Multiple event processing strategies supported

References

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages