Skip to content

MarioCroSite/hexagonal-betting-engine

Repository files navigation

Hexagonal Betting Engine

Hexagonal Betting Engine

A distributed betting settlement system built with Spring Boot, Kafka, RocketMQ, and H2 using Hexagonal Architecture.

CI Java Spring Boot Kafka RocketMQ H2


🎬 Demo

Watch the complete end-to-end flow in action:

demo.mp4

The demo showcases the entire betting settlement lifecycle: REST API β†’ Kafka publishing and consumption β†’ bet settlement logic β†’ RocketMQ publishing.


πŸ“‹ Table of Contents


🎯 Overview

The Hexagonal Betting Engine is an event-driven microservice that processes betting settlements in real-time. It consumes event outcomes from Kafka, updates bet statuses in the database, and publishes settlement notifications to RocketMQ.

Key Features

  • βœ… Event-Driven Architecture - Kafka for event ingestion, RocketMQ for settlement notifications
  • βœ… Hexagonal Architecture - Clean separation between domain, application, and infrastructure layers
  • βœ… Transactional Consistency - ACID guarantees with Spring @Transactional
  • βœ… Error Handling - Dead Letter Queue (DLQ) for failed messages
  • βœ… Comprehensive Testing - Unit tests, integration tests, and E2E tests
  • βœ… In-Memory Database - H2 for development and testing
  • βœ… OpenAPI Documentation - Swagger UI for API exploration

πŸ’Ž Implementation Highlights

  • REST API: POST /api/event-outcomes publishes event outcomes to Kafka
  • Kafka Consumer: Listens to event-outcomes topic and triggers bet settlement
  • Business Logic: Matches event outcomes with pending bets in the database
  • RocketMQ Producer: Publishes settlement results to bet-settlements topic
  • In-Memory Database: H2 with Flyway migrations and pre-seeded test data
  • Fully Dockerized: Complete environment with Kafka, RocketMQ, and monitoring dashboards
  • GitHub Actions CI: Automated testing on every push and pull request

πŸ›οΈ Architecture

This project implements Hexagonal Architecture (Ports & Adapters) to achieve high testability, maintainability, and independence from external frameworks.

graph TB
    subgraph "Application Layer"
        Controller[EventOutcomeController<br/>REST API]
    end

    subgraph "Domain Layer"
        CommandHandler[EventOutcomeCommandHandler]
        BetSettlementService[BetSettlementService]
    end

    subgraph "Infrastructure Layer"
        KafkaProducer[EventOutcomePublisherAdapter<br/>Kafka Producer]
        KafkaConsumer[EventOutcomeListenerAdapter<br/>Kafka Consumer]
        DBAdapter[BetRepositoryAdapter<br/>JPA]
        RocketMQAdapter[RocketMQBetSettlementPublisher /<br/>LoggingBetSettlementPublisher]
    end

    subgraph "External Systems"
        Kafka[(Kafka<br/>event-outcomes)]
        DB[(H2 Database)]
        RocketMQ[(RocketMQ Broker<br/>bet-settlements)]
    end

    Controller --> CommandHandler
    CommandHandler --> KafkaProducer
    KafkaProducer --> Kafka
    Kafka --> KafkaConsumer
    KafkaConsumer --> BetSettlementService
    BetSettlementService --> DBAdapter
    BetSettlementService --> RocketMQAdapter
    DBAdapter --> DB
    RocketMQAdapter --> RocketMQ

    style CommandHandler fill:#4CAF50
    style BetSettlementService fill:#4CAF50
    style Controller fill:#2196F3
Loading

Architectural Layers

Layer Responsibility Examples
Domain Core business logic, entities, ports Bet, BetSettlementService, BetRepository
Application Use cases, DTOs, exception handling EventOutcomeController, GlobalExceptionHandler
Infrastructure External integrations, adapters Kafka, RocketMQ, JPA, H2

πŸ’» Technology Stack

Category Technology Version
Language Java 21
Framework Spring Boot 4.0.2
Messaging Kafka Client 4.1.1
Messaging RocketMQ Client 5.3.2
Database H2 (In-Memory) 2.4.240
Migration Flyway 11.14.1
Build Tool Gradle 9.3
Testing JUnit 6, Mockito, AssertJ 6.0.2, 5.20.0, 3.27.6
Documentation SpringDoc OpenAPI 3.0.1
Docker Kafka (Confluent) 7.7.7
Docker RocketMQ 4.9.7

πŸ”„ Communication Flow

End-to-End Settlement Flow

sequenceDiagram
    participant Client
    participant API as REST API
    participant Kafka as Kafka Topic<br/>event-outcomes
    participant Consumer as Kafka Consumer
    participant Service as BetSettlementService
    participant DB as H2 Database
    participant RMQ as RocketMQ<br/>bet-settlements

    Client->>API: POST /api/event-outcomes
    API->>Kafka: Publish EventOutcome
    API-->>Client: 202 Accepted

    Note over Kafka,Consumer: Asynchronous Processing

    Kafka->>Consumer: Poll EventOutcome
    Consumer->>Service: settle(EventOutcome)
    Service->>DB: Find pending bets by eventId
    DB-->>Service: List<Bet>

    loop For each pending bet
        Service->>Service: Calculate status (WON/LOST)
        Service->>DB: Update bet status
        Service->>RMQ: Publish BetSettlement
    end

    Service-->>Consumer: Settlement complete
Loading

Kafka Error Handling with DLQ

graph LR
    A[Kafka Topic<br/>event-outcomes] --> B{Consumer<br/>Processing}
    B -->|Success| C[BetSettlementService]
    B -->|Failure| D[Retry 3x]
    D -->|Still Fails| E[DLQ Topic<br/>event-outcomes-dlq]
    D -->|Success| C
    C --> F[Database Update]
    C --> G[RocketMQ Publish]

    style E fill:#f44336
    style C fill:#4CAF50
Loading

πŸ“ Project Structure

src/
β”œβ”€β”€ main/
β”‚   β”œβ”€β”€ java/com/mario/hexagonalbettingengine/
β”‚   β”‚   β”œβ”€β”€ HexagonalBettingEngineApplication.java  # Main Spring Boot application
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ domain/                      # 🟒 Domain Layer (Core Business Logic)
β”‚   β”‚   β”‚   β”œβ”€β”€ betting/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Bet.java            # Domain entity
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ BetStatus.java      # Domain enum
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ BetRepository.java  # Port (interface)
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ BetSettlement.java  # Port (interface)
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ BetSettlementService.java  # Domain service
β”‚   β”‚   β”‚   β”‚   └── BetSettlementPublisher.java # Port (interface)
β”‚   β”‚   β”‚   └── eventoutcome/
β”‚   β”‚   β”‚       β”œβ”€β”€ EventOutcome.java   # Domain entity
β”‚   β”‚   β”‚       β”œβ”€β”€ EventOutcomeCommandHandler.java
β”‚   β”‚   β”‚       └── EventOutcomePublisher.java  # Port (interface)
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ application/                 # πŸ”΅ Application Layer (Use Cases)
β”‚   β”‚   β”‚   β”œβ”€β”€ eventoutcome/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ EventOutcomeController.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ request/
β”‚   β”‚   β”‚   β”‚   β”‚   └── EventOutcomeRequestDto.java
β”‚   β”‚   β”‚   β”‚   └── mapper/
β”‚   β”‚   β”‚   β”‚       └── EventOutcomeDtoMapper.java
β”‚   β”‚   β”‚   └── GlobalExceptionHandler.java
β”‚   β”‚   β”‚
β”‚   β”‚   └── infrastructure/              # 🟠 Infrastructure Layer (Adapters)
β”‚   β”‚       β”œβ”€β”€ betting/
β”‚   β”‚       β”‚   β”œβ”€β”€ BetEntity.java       # JPA entity
β”‚   β”‚       β”‚   β”œβ”€β”€ BetStatus.java       # Infrastructure enum
β”‚   β”‚       β”‚   β”œβ”€β”€ BetJpaRepository.java
β”‚   β”‚       β”‚   β”œβ”€β”€ BetRepositoryAdapter.java
β”‚   β”‚       β”‚   β”œβ”€β”€ RocketMQBetSettlementPublisher.java
β”‚   β”‚       β”‚   β”œβ”€β”€ LoggingBetSettlementPublisher.java
β”‚   β”‚       β”‚   β”œβ”€β”€ mapper/
β”‚   β”‚       β”‚   β”‚   └── BetMapper.java
β”‚   β”‚       β”‚   └── payload/
β”‚   β”‚       β”‚       β”œβ”€β”€ BetPayload.java
β”‚   β”‚       β”‚       └── BetStatus.java
β”‚   β”‚       β”œβ”€β”€ eventoutcome/
β”‚   β”‚       β”‚   β”œβ”€β”€ EventOutcomeListenerAdapter.java  # Kafka consumer
β”‚   β”‚       β”‚   β”œβ”€β”€ EventOutcomePublisherAdapter.java # Kafka producer
β”‚   β”‚       β”‚   β”œβ”€β”€ mapper/
β”‚   β”‚       β”‚   β”‚   └── EventOutcomeMapper.java
β”‚   β”‚       β”‚   └── payload/
β”‚   β”‚       β”‚       └── EventOutcomePayload.java
β”‚   β”‚       └── config/
β”‚   β”‚           β”œβ”€β”€ JacksonConfig.java
β”‚   β”‚           β”œβ”€β”€ KafkaConfig.java
β”‚   β”‚           β”œβ”€β”€ KafkaTopicConfig.java
β”‚   β”‚           └── MessagingProperties.java
β”‚   β”‚
β”‚   └── resources/
β”‚       β”œβ”€β”€ application.yml
β”‚       └── db/migration/
β”‚           β”œβ”€β”€ V1__create_bets_table.sql
β”‚           └── V2__seed_initial_bets.sql
β”‚
└── test/
    β”œβ”€β”€ java/com/mario/hexagonalbettingengine/
    β”‚   β”œβ”€β”€ BaseIT.java                  # Base integration test class
    β”‚   β”œβ”€β”€ BetSettlementEndToEndIT.java # E2E test
    β”‚   β”‚
    β”‚   β”œβ”€β”€ domain/                      # 🟒 Domain unit tests
    β”‚   β”‚   β”œβ”€β”€ betting/
    β”‚   β”‚   β”‚   β”œβ”€β”€ BetTest.java
    β”‚   β”‚   β”‚   └── BetSettlementServiceTest.java
    β”‚   β”‚   └── eventoutcome/
    β”‚   β”‚       └── EventOutcomeCommandHandlerTest.java
    β”‚   β”‚
    β”‚   β”œβ”€β”€ application/                 # πŸ”΅ Application unit/integration tests
    β”‚   β”‚   └── eventoutcome/
    β”‚   β”‚       β”œβ”€β”€ EventOutcomeControllerIT.java
    β”‚   β”‚       β”œβ”€β”€ EventOutcomeControllerTest.java
    β”‚   β”‚       └── mapper/
    β”‚   β”‚           └── EventOutcomeDtoMapperTest.java
    β”‚   β”‚
    β”‚   β”œβ”€β”€ infrastructure/              # 🟠 Infrastructure unit/integration tests
    β”‚   β”‚   β”œβ”€β”€ betting/
    β”‚   β”‚   β”‚   β”œβ”€β”€ BetRepositoryAdapterIT.java
    β”‚   β”‚   β”‚   β”œβ”€β”€ BetRepositoryAdapterTest.java
    β”‚   β”‚   β”‚   β”œβ”€β”€ LoggingBetSettlementPublisherTest.java
    β”‚   β”‚   β”‚   β”œβ”€β”€ RocketMQBetSettlementPublisherTest.java
    β”‚   β”‚   β”‚   └── mapper/
    β”‚   β”‚   β”‚       └── BetMapperTest.java
    β”‚   β”‚   └── eventoutcome/
    β”‚   β”‚       β”œβ”€β”€ EventOutcomeKafkaConsumerIT.java
    β”‚   β”‚       β”œβ”€β”€ EventOutcomeKafkaProducerIT.java
    β”‚   β”‚       β”œβ”€β”€ EventOutcomeListenerAdapterTest.java
    β”‚   β”‚       β”œβ”€β”€ EventOutcomePublisherAdapterTest.java
    β”‚   β”‚       └── mapper/
    β”‚   β”‚           └── EventOutcomeMapperTest.java
    β”‚   β”‚
    β”‚   └── fixtures/                    # Test data builders
    β”‚       β”œβ”€β”€ BetEntityFixtures.java
    β”‚       β”œβ”€β”€ BetFixtures.java
    β”‚       β”œβ”€β”€ EventOutcomeFixtures.java
    β”‚       └── EventOutcomeRequestDtoFixtures.java
    β”‚
    └── resources/
        └── application-test.yml

πŸš€ Getting Started

Prerequisites

  • Docker & Docker Compose 🐳
  • Java 21 (for local development)
  • Gradle 9+ (optional, wrapper included)

Clone the Repository

git clone https://github.com/MarioCroSite/hexagonal-betting-engine.git
cd hexagonal-betting-engine

⚑ Option 1: Local Development (Recommended)

Best for: Daily development, debugging, and rapid iteration with hot reload.

1️⃣ Start Infrastructure Services

Start Kafka, RocketMQ, and their monitoring UIs using Docker Compose:

docker-compose up -d

This will start:

  • Kafka on localhost:9092
  • Kafka UI on http://localhost:8090
  • RocketMQ NameServer on localhost:9876
  • RocketMQ Broker on localhost:10911
  • RocketMQ Dashboard on http://localhost:8082

2️⃣ Run Application Locally

./gradlew bootRun
  • Application starts on http://localhost:8080
  • H2 Console available at http://localhost:8080/h2-console
  • Swagger UI available at http://localhost:8080/swagger-ui/index.html
  • Changes reload automatically with Spring DevTools

🐳 Option 2: Full Docker Stack (Advanced)

Best for: Production-like environment, E2E testing, CI/CD pipelines, and demonstrations.

1️⃣ Build Application Docker Image

docker build -t hexagonal-betting-engine:latest .

Image Details:

  • Multi-stage build (Eclipse Temurin 21 β†’ Amazon Corretto 21)
  • Optimized layer caching for dependencies
  • Alpine-based for minimal footprint
  • JVM tuned for containerized environments

2️⃣ Start Full Stack

docker-compose --profile full-stack up -d

This will start all services including the application container.

3️⃣ View Application Logs

docker logs -f betting_app

Example Log Output:

2026-02-04T22:15:32.145+01:00  INFO 1 --- [nio-8080-exec-3] c.m.h.a.e.EventOutcomeController         : Received request: EventOutcomeRequestDto[eventId=match-100, eventName=Real Madrid vs Barcelona, eventWinnerId=REAL_MADRID]

2026-02-04T22:15:32.498+01:00  INFO 1 --- [gine-producer-1] c.m.h.i.e.EventOutcomePublisherAdapter   : Event match-100 published. Partition: 0, Offset: 0

2026-02-04T22:15:32.512+01:00  INFO 1 --- [-consumer-0-C-1] c.m.h.i.e.EventOutcomeListenerAdapter    : Received event outcome: eventId=match-100, eventName=Real Madrid vs Barcelona, winnerId=REAL_MADRID

2026-02-04T22:15:32.784+01:00  INFO 1 --- [-consumer-0-C-1] c.m.h.i.b.RocketMQBetSettlementPublisher : Bet settlement published to bet-settlements topic: BetPayload[betId=b-001, userId=user-1, eventId=match-100, eventMarketId=1x2, eventWinnerId=REAL_MADRID, betAmount=10.00, status=WON, settledAt=2026-02-04T21:15:32.784219Z]

2026-02-04T22:15:32.801+01:00  INFO 1 --- [-consumer-0-C-1] c.m.h.i.b.RocketMQBetSettlementPublisher : Bet settlement published to bet-settlements topic: BetPayload[betId=b-002, userId=user-2, eventId=match-100, eventMarketId=1x2, eventWinnerId=BARCELONA, betAmount=25.50, status=LOST, settledAt=2026-02-04T21:15:32.801043Z]

Log Flow Breakdown:

  1. Controller receives REST request β†’ logs incoming DTO
  2. Kafka Producer publishes event β†’ logs partition/offset
  3. Kafka Consumer receives event β†’ logs event details
  4. Settlement Publisher processes each bet β†’ logs settlement payload
  • Application containerized and running on http://localhost:8080
  • All services isolated in Docker network betting-net
  • Production-ready setup with observability through structured logging

βš™οΈ Configuration

RocketMQ Operating Modes

The system supports two different modes for publishing bet settlements using Conditional Bean Registration (@ConditionalOnProperty), allowing you to toggle between logging-only mode and real RocketMQ connectivity.

Toggle Switch

In application.yml, control whether the application communicates with a real RocketMQ broker:

app:
  messaging:
    rocketmq:
      enabled: false # false = Logging only, true = Real RocketMQ
enabled Active Implementation Behavior
false LoggingBetSettlementPublisher Settlements are printed to console/logs. Best for rapid development without Docker stack.
true RocketMQBetSettlementPublisher Messages sent to live RocketMQ broker. Required for full end-to-end testing.

RocketMQ Networking Setup

⚠️ Important: RocketMQ requires specific networking setup to bridge Docker containers and your host machine.

Version Choice:

  • Uses RocketMQ 4.9.7 for maximum stability and compatibility with Dashboard
  • Avoids the complexity of gRPC Proxy introduced in version 5.x

The brokerIP1 Requirement:

  • The RocketMQ broker must broadcast an IP address reachable by your application
  • Configured in rocketmq/broker.conf file
  • Recommendation: Use your actual LAN IP (e.g., 192.168.x.x) or host.docker.internal
  • This ensures the application on your host can "handshake" with the broker inside Docker

Troubleshooting:

  • If you encounter connection timeouts, verify that your machine's IP matches the one in broker.conf
  • Check Docker network configuration: docker network inspect betting-net

πŸ“š API Documentation

Swagger UI

Access the interactive API documentation at:

http://localhost:8080/swagger-ui/index.html

Swagger UI

Core Endpoint

🎲 Publish Event Outcome

Publishes an event outcome to Kafka, triggering bet settlement for all pending bets on that event.

curl -X POST http://localhost:8080/api/event-outcomes \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "match-100",
    "eventName": "Real Madrid vs Barcelona",
    "eventWinnerId": "REAL_MADRID"
  }'

Response:

HTTP/1.1 202 Accepted

πŸ§ͺ Run Test Scenarios

The application comes with pre-seeded pending bets via Flyway migration (V2__seed_initial_bets.sql). Below are three realistic test scenarios to demonstrate the end-to-end bet settlement flow:


Scenario 1: El ClÑsico ⚽

Context: Spain's biggest football rivalry - Real Madrid vs Barcelona

Seeded Bets:

Bet ID User Predicted Winner Amount Status
b-001 user-1 REAL_MADRID €10.00 PENDING
b-002 user-2 BARCELONA €25.50 PENDING
b-003 user-3 DRAW €5.00 PENDING
b-004 user-4 REAL_MADRID €100.00 PENDING

Trigger Event Outcome:

curl -X POST http://localhost:8080/api/event-outcomes \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "match-100",
    "eventName": "Real Madrid vs Barcelona",
    "eventWinnerId": "REAL_MADRID"
  }'

Expected Result:

  • βœ… 2 WON: b-001, b-004 (predicted REAL_MADRID correctly)
  • ❌ 2 LOST: b-002, b-003 (predicted BARCELONA and DRAW)
  • πŸ“€ 4 settlement messages published to RocketMQ

Scenario 2: Champions League Thriller πŸ†

Context: European club football's elite competition - Liverpool vs AC Milan

Seeded Bets:

Bet ID User Predicted Winner Amount Status
b-005 user-1 LIVERPOOL €15.00 PENDING
b-006 user-5 MILAN €40.00 PENDING
b-007 user-2 LIVERPOOL €12.00 PENDING

Trigger Event Outcome:

curl -X POST http://localhost:8080/api/event-outcomes \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "match-200",
    "eventName": "Liverpool vs Milan",
    "eventWinnerId": "MILAN"
  }'

Expected Result:

  • βœ… 1 WON: b-006 (predicted MILAN correctly)
  • ❌ 2 LOST: b-005, b-007 (predicted LIVERPOOL)
  • πŸ“€ 3 settlement messages published to RocketMQ

Scenario 3: NBA Showdown πŸ€

Context: Historic basketball rivalry - Los Angeles Lakers vs Boston Celtics

Seeded Bets:

Bet ID User Predicted Winner Amount Status
b-008 user-6 LAKERS $50.00 PENDING
b-009 user-7 CELTICS $30.00 PENDING
b-010 user-1 LAKERS $20.00 PENDING

Trigger Event Outcome:

curl -X POST http://localhost:8080/api/event-outcomes \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "match-300",
    "eventName": "Lakers vs Celtics",
    "eventWinnerId": "LAKERS"
  }'

Expected Result:

  • βœ… 2 WON: b-008, b-010 (predicted LAKERS correctly)
  • ❌ 1 LOST: b-009 (predicted CELTICS)
  • πŸ“€ 3 settlement messages published to RocketMQ

πŸ’‘ Tip: After triggering any scenario, verify settlements in:

  • H2 Console (http://localhost:8080/h2-console) - Query: SELECT * FROM bets WHERE event_id = 'match-100';
  • RocketMQ Dashboard (http://localhost:8082) - Check bet-settlements topic messages
  • Application Logs - Watch for settlement processing confirmations

πŸ§ͺ Testing

Test Coverage

The project has comprehensive test coverage across all architectural layers:

Test Type Coverage
Unit Tests Domain & Application layers
Integration Tests Kafka, RocketMQ, Database
E2E Tests Full flow: Kafka β†’ DB β†’ RocketMQ

πŸ€– Automated CI

This project uses GitHub Actions to automatically run tests on every push and pull_request. This ensures high code quality and guarantees that no broken code is merged into the main branch.

Running Tests

Run All Tests

./gradlew test

Run Only Unit Tests

./gradlew test --tests "*Test"

Run Only Integration Tests

./gradlew test --tests "*IT"

Test Report

After running tests, view the HTML report:

open build/reports/tests/test/index.html

πŸ“Š Monitoring

Kafka UI

Monitor Kafka topics, consumer groups, and messages:

http://localhost:8090

Navigation:

  1. Go to Topics β†’ event-outcomes
  2. Click Messages to see consumed events
  3. Check Consumer Groups for processing status

Event Outcomes Topic

Kafka Event Outcomes Topic

Dead Letter Queue (DLQ)

Kafka DLQ Topic

RocketMQ Dashboard

Monitor RocketMQ topics and message flows:

http://localhost:8082

RocketMQ Dashboard

Features:

  • View topic statistics
  • Monitor message traces
  • Check consumer status

H2 Database Console

Access the H2 in-memory database console for development and debugging:

http://localhost:8080/h2-console

H2 Console

Connection details:

  • JDBC URL: jdbc:h2:mem:betting-db
  • Username: sa
  • Password: (leave empty)

🎨 Design Decisions

1. Hexagonal Architecture (Ports & Adapters)

Why: Achieve independence from frameworks and external systems.

  • Domain Layer contains pure business logic with no external dependencies
  • Ports (interfaces) define contracts between layers
  • Adapters (implementations) handle external integrations (Kafka, RocketMQ, JPA)

Benefits:

  • βœ… Testability: Domain logic can be tested without infrastructure
  • βœ… Flexibility: Easy to swap adapters (e.g., replace Kafka with RabbitMQ)
  • βœ… Maintainability: Clear separation of concerns

2. Event-Driven Architecture

Why: Decouple event ingestion from settlement processing.

  • REST API publishes events to Kafka (non-blocking)
  • Kafka consumer processes events asynchronously
  • RocketMQ publishes settlement notifications to downstream systems

Benefits:

  • βœ… Scalability: Consumers can scale independently
  • βœ… Resilience: Failed messages go to DLQ for manual review
  • βœ… Performance: Non-blocking API responses

3. Domain-Driven Design (DDD)

Why: Model complex business rules explicitly.

  • Bet aggregate encapsulates bet validation and status transitions
  • BetSettlementService orchestrates settlement logic
  • Domain events could be added for audit trails (future enhancement)

4. Transactional Outbox Pattern (Implicit)

Why: Ensure consistency between database updates and message publishing.

  • Settlement updates and RocketMQ publishing happen within a single @Transactional boundary
  • If RocketMQ fails, database transaction rolls back

5. Configuration-Driven Design

Why: Externalize configuration for different environments.

app:
  messaging:
    rocketmq:
      enabled: true
      topic: bet-settlements
    kafka:
      event-outcomes:
        topic: event-outcomes
        dlq-topic: event-outcomes-dlq
        retry-attempts: 3

6. Dead Letter Queue (DLQ) Pattern

Why: Handle poison messages without blocking the consumer.

  • Failed messages are retried 3 times with exponential backoff
  • After 3 failures, message is sent to event-outcomes-dlq
  • DLQ messages can be manually reviewed and reprocessed

7. Type-Safe Configuration with Records

Why: Immutable, compile-time-safe configuration.

@ConfigurationProperties(prefix = "app.messaging")
public record MessagingProperties(
    RocketMqConfig rocketmq,
    KafkaConfig kafka
) { }

πŸš€ Future Improvements

This project currently focuses on the Event Outcome Settlement flow, demonstrating how bets are automatically settled when sports events conclude. To fully support a production betting platform, the following enhancements are planned:

1. Bet Management API

Currently, bets are pre-seeded via Flyway migrations for demonstration purposes. A production system would require:

POST /api/bets - Place a Bet

Request:

{
  "userId": "user-123",
  "eventId": "match-500",
  "eventMarketId": "1x2",
  "eventWinnerId": "REAL_MADRID",
  "betAmount": 50.00
}

Response:

{
  "betId": "b-101",
  "status": "PENDING",
  "placedAt": "2026-02-04T21:30:00Z"
}

Domain Considerations:

  • Validation: Ensure event exists, market is open, bet amount meets minimum requirements
  • Idempotency: Prevent duplicate bets using idempotency keys
  • Balance Check: Integrate with wallet service to verify user funds

πŸ‘€ Author

Mario Vegh


About

🎲 Event-driven betting settlement engine built with Spring Boot, Kafka, RocketMQ, and H2. Implementing Hexagonal Architecture, DDD, and a robust CI pipeline.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors