A distributed betting settlement system built with Spring Boot, Kafka, RocketMQ, and H2 using Hexagonal Architecture.
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.
- Overview
- Architecture
- Technology Stack
- Communication Flow
- Project Structure
- Getting Started
- API Documentation
- Testing
- Monitoring
- Design Decisions
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.
- β 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
- REST API:
POST /api/event-outcomespublishes event outcomes to Kafka - Kafka Consumer: Listens to
event-outcomestopic and triggers bet settlement - Business Logic: Matches event outcomes with pending bets in the database
- RocketMQ Producer: Publishes settlement results to
bet-settlementstopic - 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
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
| 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 |
| 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 |
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
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
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
- Docker & Docker Compose π³
- Java 21 (for local development)
- Gradle 9+ (optional, wrapper included)
git clone https://github.com/MarioCroSite/hexagonal-betting-engine.git
cd hexagonal-betting-engineBest for: Daily development, debugging, and rapid iteration with hot reload.
Start Kafka, RocketMQ, and their monitoring UIs using Docker Compose:
docker-compose up -dThis 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
./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
Best for: Production-like environment, E2E testing, CI/CD pipelines, and demonstrations.
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
docker-compose --profile full-stack up -dThis will start all services including the application container.
docker logs -f betting_appExample 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:
- Controller receives REST request β logs incoming DTO
- Kafka Producer publishes event β logs partition/offset
- Kafka Consumer receives event β logs event details
- 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
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.
In application.yml, control whether the application communicates with a real RocketMQ broker:
app:
messaging:
rocketmq:
enabled: false # false = Logging only, true = Real RocketMQenabled |
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. |
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.conffile - Recommendation: Use your actual LAN IP (e.g.,
192.168.x.x) orhost.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
Access the interactive API documentation at:
http://localhost:8080/swagger-ui/index.html
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
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:
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
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
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) - Checkbet-settlementstopic messages- Application Logs - Watch for settlement processing confirmations
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 |
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.
./gradlew test./gradlew test --tests "*Test"./gradlew test --tests "*IT"After running tests, view the HTML report:
open build/reports/tests/test/index.htmlMonitor Kafka topics, consumer groups, and messages:
http://localhost:8090
Navigation:
- Go to Topics β
event-outcomes - Click Messages to see consumed events
- Check Consumer Groups for processing status
Monitor RocketMQ topics and message flows:
http://localhost:8082
Features:
- View topic statistics
- Monitor message traces
- Check consumer status
Access the H2 in-memory database console for development and debugging:
http://localhost:8080/h2-console
Connection details:
- JDBC URL:
jdbc:h2:mem:betting-db - Username:
sa - Password: (leave empty)
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
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
Why: Model complex business rules explicitly.
Betaggregate encapsulates bet validation and status transitionsBetSettlementServiceorchestrates settlement logic- Domain events could be added for audit trails (future enhancement)
Why: Ensure consistency between database updates and message publishing.
- Settlement updates and RocketMQ publishing happen within a single
@Transactionalboundary - If RocketMQ fails, database transaction rolls back
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: 3Why: 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
Why: Immutable, compile-time-safe configuration.
@ConfigurationProperties(prefix = "app.messaging")
public record MessagingProperties(
RocketMqConfig rocketmq,
KafkaConfig kafka
) { }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:
Currently, bets are pre-seeded via Flyway migrations for demonstration purposes. A production system would require:
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
Mario Vegh
- π GitHub: @MarioCroSite
- πΌ LinkedIn: mvegh
- π Repository: hexagonal-betting-engine





