A banking transaction ledger built with Clean Architecture, demonstrating a robust account transaction system with CQRS-style separation of read and write concerns.
WARNING!
Integration tests require a container runtime (Docker, Podman, etc.) for Testcontainers. Tests will be skipped if no container runtime is configured.
- Architecture Overview
- Key Design Decisions
- Tech Stack
- Getting Started
- API Documentation
- Testing
- Observability
- Database
- Production Considerations
This project follows Clean Architecture principles with clear separation between domain logic, application use cases, and infrastructure concerns. The application is organized as a multi-module Gradle project:
ledger/
│
├── domain/ # Core business logic & entities
│ ├── account/ # Account aggregate
│ ├── transaction/ # Transaction aggregate
│ └── pagination/ # Shared pagination utilities
│
├── application/ # Use cases & orchestration
│ └── account/
│ └── transaction/
│ ├── create/ # Create transaction use case
│ └── list/ # List transactions use case
│
└── infrastructure/ # External adapters
├── adapter/ # JPA persistence adapters
├── configuration/ # Spring configuration
└── rest/ # REST API controllers
- Clean Architecture: Enforced dependency rules with domain at the center
- CQRS-Style: Separate read/write data sources with routing capability
- Use Case Pattern: Application logic encapsulated in discrete use cases
- Gateway Pattern: Domain interfaces implemented by infrastructure adapters
All architectural decisions are documented in Architecture Decision Records (ADRs):
- Java 25 - Latest LTS version
- Spring Boot 4.0.1 - Framework foundation
- PostgreSQL - SQL ACID database
- Flyway - Database migration management
- Gradle - Build automation
- Spring Data JPA - Data access layer
- Hibernate - ORM implementation
- Custom DataSource Routing - Read/write database separation
- OpenTelemetry - Distributed tracing
- Micrometer - Metrics instrumentation
- Logstash Encoder - Structured JSON logging
- Spring Boot Actuator - Health checks and metrics
- Grafana LGTM - Logs, graphs, traces, and metrics visualization
- JUnit 5 - Test framework
- Mockito - Mocking library
- Testcontainers - Integration testing with PostgreSQL
- JaCoCo - Java Code Coverage reporting
- SpringDoc OpenAPI - API documentation (Swagger UI)
- Jakarta Bean Validation - Request validation
- Bruno - API collection for manual testing
- Java 25+ (SDKMAN recommended)
- Container runtime: Docker, Podman, or compatible alternative
- Make (optional, for convenience commands)
# Setup environment files
make setup-env
# Start dependencies (postgresql and otel-lgtm)
make compose-up-deps
# Run the application locally (for dev profile run migrations as well)
make runThis will:
- Copy
.env.sampleto.env(if not exists) - Start PostgreSQL and OpenTelemetry collector via Container Compose
- Start the application with Spring DevTools enabled
# Setup all environment files
make setup-env setup-env-container
# Start full stack (postgresql + otel-lgtm + ledger app and migrations if dev profile set)
make compose-up-full# Check all available commands
make helpThe application uses the following environment variables (defaults in .env.sample):
# Spring Profile
SPRING_PROFILES_ACTIVE=dev
# Server
HTTP_PORT=8080
# Database - Read-Only (queries)
DBRO_URL=jdbc:postgresql://localhost:5432/ledger
DBRO_APPLICATION_USER=root
DBRO_APPLICATION_PASSWORD=root
# Database - Read-Write (commands)
DBRW_URL=jdbc:postgresql://localhost:5432/ledger
DBRW_APPLICATION_USER=root
DBRW_APPLICATION_PASSWORD=root
# Database - Migrations
DBRW_MIGRATIONS_USER=root
DBRW_MIGRATIONS_PASSWORD=root
# OpenTelemetry
OTEL_EXPORTER_OTLP_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318The Makefile supports multiple container runtimes (docker, podman, nerdctl, etc.):
# Using Podman
export CONTAINER_RUNTIME=podman
make compose-up-deps
# Or inline
CONTAINER_RUNTIME=podman make compose-up-depshttp://localhost:8080/api
Initiates an outbound transaction from a given account to a beneficiary.
Endpoint: POST /v1/accounts/{id}/transactions
Path Parameters:
id(UUID, required) - Source account ID
Request Body:
{
"beneficiaryName": "John Doe",
"amount": 5000,
"paymentReference": "Invoice #12345"
}Field Descriptions:
beneficiaryName(string, required) - Recipient's full nameamount(integer, required) - Amount in minor units (cents)paymentReference(string, optional) - Optional payment reference
Example Request:
curl --request POST \
--url http://localhost:8080/api/v1/accounts/550e8400-e29b-41d4-a716-446655440000/transactions \
--header 'Content-Type: application/json' \
--data '{
"beneficiaryName": "John Doe",
"amount": 5000,
"paymentReference": "Invoice #12345"
}'Response Codes:
200 OK- Transaction created successfully422 Unprocessable Entity- Validation error (e.g., insufficient funds, invalid account)500 Internal Server Error- Internal Server Error
Business Rules:
- Account must exist and have sufficient balance
- Amount must be positive
- Account balance is updated atomically with pessimistic locking
Lists all transactions for a given account, ordered by creation date (most recent first).
Endpoint: GET /v1/accounts/{id}/transactions
Path Parameters:
id(UUID, required) - Account ID
Query Parameters:
page(integer, optional, default: 0) - Page number (zero-indexed)perPage(integer, optional, default: 10) - Items per pagedir(string, optional, default: DESC) - Sort direction (ASC or DESC)
Example Request:
curl --request GET \
--url 'http://localhost:8080/api/v1/accounts/550e8400-e29b-41d4-a716-446655440000/transactions?page=0&perPage=10&dir=DESC'Example Response:
{
"currentPage": 0,
"perPage": 10,
"total": 25,
"items": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "DEBIT",
"amount": 5000,
"paymentReference": "Invoice #12345"
},
{
"id": "8d1f7780-8536-51ef-a55c-f18gd2g01bf8",
"type": "CREDIT",
"amount": 2500,
"paymentReference": "Payment for services"
}
]
}Response Codes:
200 OK- Transactions listed successfully404 Not Found- Not Found Account ID422 Unprocessable Entity- Validation error (e.g., invalid account ID)500 Internal Server Error- Server error
OpenAPI/Swagger UI available at:
http://localhost:8080/api/swagger-ui.html
OpenAPI specification:
http://localhost:8080/api/api-docs
Pre-configured API requests are available in the .bruno/ directory. Use Bruno to import and test the API endpoints.
# Using Make
make test
# Using Gradle directly
./gradlew clean testAfter running tests, view the JaCoCo coverage report:
# Generate reports automatically when running tests
./gradlew test
# Open reports in browser
open infrastructure/build/reports/jacoco/test/html/index.html # macOS
xdg-open infrastructure/build/reports/jacoco/test/html/index.html # LinuxThe project includes comprehensive test coverage:
-
Unit Tests: Domain entities and use cases with Mockito
domain/src/test/- Domain entity validation and business rulesapplication/src/test/- Use case logic with mocked gateways
-
Integration Tests: Repository layer with Testcontainers
infrastructure/src/test/- JPA repositories with real PostgreSQL
-
End-to-End Tests: REST API with Spring MockMvc
infrastructure/src/test/- Full request/response cycle testing
- Container runtime (Docker/Podman) must be running for integration tests
- Tests use Testcontainers to spin up PostgreSQL automatically
- If no container runtime is available, integration tests will be skipped
curl http://localhost:8080/api/actuator/healthExample Response:
{
"status": "UP",
"components": {
"db": {
"status": "UP"
},
"diskSpace": {
"status": "UP"
}
}
}Application metrics exposed in:
curl http://localhost:8080/api/actuator/metricsAvailable Metrics:
http_server_requests_*- HTTP request metricsjvm_memory_*- JVM memory usagejvm_threads_*- Thread pool metricshikaricp_connections_*- Database connection pool metricsprocess_cpu_usage- CPU utilization
The application exports traces to Grafana LGTM stack:
Access Grafana:
http://localhost:3000
Username: admin
Password: admin
Features:
- Distributed trace visualization
- Request latency analysis
- Database query performance
- Service dependency mapping
Application uses structured JSON logging with automatic trace ID propagation:
{
"@timestamp": "2025-01-06T10:30:00.000Z",
"level": "INFO",
"logger_name": "com.banking.ledger.infrastructure.rest.controller.AccountsController",
"message": "Successfully created transaction",
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"transaction_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"amount": 5000,
"trace_id": "a1b2c3d4e5f6g7h8",
"span_id": "1234567890abcdef",
"service.name": "ledger"
}/actuator/health- Application health status/actuator/info- Build information/actuator/metrics- Available metrics list
Flyway migrations are located in infrastructure/src/main/resources/db/migration/:
V001__create_schema_ledger.sql- Create ledger schemaV002__create_enum_currency.sql- Currency enum typeV003__create_table_accounts.sql- Accounts tableV004__create_table_transactions.sql- Transactions table
Migrations run automatically on application startup or can be run manually:
./gradlew :infrastructure:flywayMigrateThe application uses custom DataSource routing to separate read and write operations:
- Write DataSource: All
@Transactionalmethods with create/update operations - Read DataSource: Read-only queries for listing transactions
This architecture enables:
- Future horizontal scaling with read replicas
- Performance optimization by load balancing reads
- Clear separation of concerns between commands and queries
- Structured Logging: JSON logs with trace context for centralized log aggregation
- Health Checks: Spring Actuator endpoints for container orchestration
- Metrics Export: Prometheus-compatible metrics for monitoring
- Distributed Tracing: OpenTelemetry integration for request tracing
- Database Migrations: Flyway for safe, versioned schema changes
- Validation: Jakarta Bean Validation on API inputs and domain validations
- Error Handling: Structured error responses with appropriate HTTP status codes
- API Documentation: OpenAPI/Swagger for API discovery
These improvements would be valuable for a production-ready system:
- Authentication & Authorization - OAuth2/JWT integration
- API Rate Limiting - Bucket4j or similar for abuse prevention
- Security Headers - HSTS, CSP, X-Frame-Options
- Caching Layer - Redis for frequently accessed data
- Message Queue - Async transaction processing with Kafka/RabbitMQ
- API Gateway - Centralized routing, authentication, and rate limiting
- Circuit Breakers - Resilience4j for failure isolation
- Retry Policies - Exponential backoff for transient failures
- Timeout Configuration - Request-level timeout strategies
- Bulkhead Pattern - Resource isolation for critical operations
- Graceful Degradation - Fallback mechanisms for service outages
- Custom Business Metrics - Transaction volume, success rates, latency percentiles
- Alerting - Alerts for critical conditions
- APM Integration - New Relic, Datadog, or Dynatrace
- Distributed Trace Sampling - Intelligent trace sampling for high-volume systems
- Contract Testing - For API compatibility guarantees
- Performance Testing - Gatling or JMeter for load testing
- Chaos Engineering - Failure injection for resilience validation
- CI/CD Pipeline - Automated testing, building, and deployment
- Container Orchestration - Kubernetes for production deployment
- Blue-Green Deployment - Zero-downtime deployments
- Infrastructure as Code - Terraform or similar for reproducible environments
- Audit Logging - Immutable audit trail for regulatory compliance
- Data Retention Policies - Automated data archival and purging
- Multi-region Support - Data residency requirements
- Multiple currency accounts with wallets for each one
- Account identifier for transactions instead of using name and origin currency