A Spring Boot portfolio project for a compact banking-style ledger with double-entry accounting, ACID transaction handling, auditability, Oracle-oriented persistence, reconciliation, and event publishing.
The backend API lives in banking-ledger-api.
- Tech Stack
- Feature Highlights
- Quickstart
- Project Structure
- End-State Architecture
- Prerequisites
- Environment Setup
- Run Development Infrastructure
- Run The API Locally
- CI And Quality Gates
- Developer Commands
- Demo Flow
- Spring Profiles
- Oracle Database
- CloudBeaver Database Manager
- Kafka
- Production Compose
- Build And Test
- Database Migrations
- Documentation
- Development Notes
- Java 21
- Spring Boot 3.5.x
- Spring Web
- Spring Security and OAuth2 Resource Server
- Spring Data JPA / Hibernate
- Oracle Database Free 23c
- Flyway
- Kafka
- Docker Compose
- GitHub Actions
- CloudBeaver browser database manager
- Double-entry ledger posting with balanced journal entries and append-only financial history.
- Account, transfer, reversal, adjustment, reconciliation, audit, ledger investigation, and outbox APIs.
- ACID transaction boundaries for ledger records, cached balances, audit rows, idempotency records, and outbox events.
- Pessimistic account locking and idempotency keys to protect money movement under retries and concurrency.
- JWT bearer authentication, role authorization, account ownership checks, and structured security errors.
- Transactional outbox publishing to Kafka with retry, dead-letter handling, and protected requeue operations.
- Oracle-oriented Flyway schema, SQL reports, deterministic dev seed data, OpenAPI docs, and runnable HTTP demo flow.
- GitHub Actions quality gates for Maven verification, Oracle-backed integration checks, coverage, dependency review, secret scanning, and container scanning.
Run the backend locally from a clean checkout:
cd banking-ledger-api
cp .env.example .env
make deps-up
make runThen open:
http://localhost:8080/swagger-ui/index.html
Run the guided API demo from banking-ledger-api/http/demo-flow.http, or issue sample tokens manually:
make token-customer
make token-ops.
├── .github/
│ └── workflows/
│ └── backend-ci.yml
├── banking-ledger-api/
│ ├── compose.ci.yaml
│ ├── compose.dev.yaml
│ ├── compose.prod.yaml
│ ├── compose.yaml
│ ├── Dockerfile
│ ├── .env.example
│ ├── pom.xml
│ └── src/
└── docs/
├── adr/
├── backend/
├── design/
├── diagrams/
├── frontend/
└── operations/
The project is designed as a modular Spring Boot core banking ledger. The API layer accepts operational requests, application services coordinate transactional workflows, domain policies enforce financial invariants, and Oracle remains the system of record for accounts, transfers, ledger postings, audit events, idempotency records, and outbox events.
flowchart TB
client["API Clients\nCustomer apps, teller tools, ops tools, service clients"]
auth["Security Boundary\nJWT authentication, role checks, ownership checks"]
api["REST API Layer\nAccounts, transfers, ledger operations, reversals, reconciliation, audit"]
app["Application Services\nUse cases, transaction boundaries, idempotency, orchestration"]
domain["Domain Layer\nDouble-entry rules, account status policy, transfer validation, reversal rules"]
ledger["Ledger Posting Engine\nBalanced journals, postings, cached balance updates, append-only writes"]
persistence["Persistence Layer\nSpring Data JPA repositories, entity mappers, Flyway-managed schema"]
oracle[("Oracle Database\nAccounts, ledger transactions, journal entries, postings,\ntransfers, audit events, idempotency records, outbox events")]
outbox["Transactional Outbox\nPending events committed with business data"]
publisher["Outbox Publisher\nRetry, dead-letter handling, metrics"]
kafka[("Kafka\nLedgerTransactionPosted, LedgerTransactionReversed,\nAccountBalanceChanged, ReconciliationMismatchFound")]
audit["Audit Trail\nActor, role, channel, correlation id, entity, payload"]
reconciliation["Reconciliation Jobs\nSettlement import, ledger comparison, mismatch reporting"]
reporting["Reporting and Investigation\nAccount history, ops ledger lookup, audit queries, SQL reports"]
observability["Observability\nCorrelation IDs, structured errors, actuator health, metrics"]
client --> auth
auth --> api
api --> app
app --> domain
domain --> ledger
app --> ledger
ledger --> persistence
app --> persistence
persistence --> oracle
app --> audit
audit --> oracle
app --> outbox
outbox --> oracle
publisher --> outbox
publisher --> kafka
reconciliation --> persistence
reconciliation --> oracle
reporting --> persistence
reporting --> oracle
api --> observability
app --> observability
Key architectural properties:
- Double-entry posting is the only normal path for balance-changing financial activity.
- Posted ledger records are append-only; corrections use reversals or adjustment entries.
- Account balances are cached views updated inside the same database transaction as postings.
- Idempotency protects write APIs from duplicate client retries.
- Audit and outbox records are committed atomically with financial workflows.
- Kafka publishing is decoupled through the transactional outbox so database commits are never dependent on broker availability.
- Oracle constraints remain a final safety net for amount, status, currency, and relationship invariants.
- JDK 21
- Docker Desktop or Docker Engine with Docker Compose
- Git
The Maven wrapper is included, so a local Maven installation is not required.
From the API folder:
cd banking-ledger-api
cp .env.example .envThe default development values are:
DB_URL=jdbc:oracle:thin:@localhost:1521/FREEPDB1
DB_USERNAME=ledger_dev
DB_PASSWORD=ledger_dev_password
ORACLE_PASSWORD=oracle_admin_password
CLOUDBEAVER_PORT=8978
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
Do not commit .env.
Start Oracle, CloudBeaver, and Kafka:
cd banking-ledger-api
docker compose -f compose.dev.yaml up -dCheck container status:
docker compose -f compose.dev.yaml psOpen CloudBeaver:
http://localhost:8978
Development CloudBeaver login:
Username: cbadmin
Password: cbadmin_Password1
Create an Oracle connection in CloudBeaver with:
Host: oracle
Port: 1521
Service: FREEPDB1
Username: ledger_dev
Password: ledger_dev_password
CloudBeaver only applies the seeded admin login during first workspace initialization. If the CloudBeaver volume was already initialized, reset it before starting again:
docker compose -f compose.dev.yaml down -v
docker compose -f compose.dev.yaml up -dStop development infrastructure:
docker compose -f compose.dev.yaml downRemove development volumes if you need a clean database:
docker compose -f compose.dev.yaml down -vStart the development infrastructure first, then run:
cd banking-ledger-api
./mvnw spring-boot:runThe application uses the dev profile by default.
API base URL:
http://localhost:8080
Actuator health endpoint:
http://localhost:8080/actuator/health
OpenAPI documentation:
http://localhost:8080/swagger-ui/index.html
http://localhost:8080/v3/api-docs
The local demo collection is banking-ledger-api/http/demo-flow.http.
Backend CI runs through Backend CI on pull requests and pushes to main.
The workflow:
- Starts Oracle Free and Kafka with compose.ci.yaml.
- Runs Maven validation, tests, verification, and jar packaging.
- Generates JaCoCo coverage reports.
- Builds the Docker image with OCI labels.
- Runs dependency review, Gitleaks, and Trivy.
- Uploads test, coverage, and scan artifacts where useful.
OWASP dependency-check runs in Dependency Check on a weekly schedule or manual dispatch. Configure an NVD_API_KEY repository secret to avoid slow anonymous NVD updates.
Local CI parity commands:
cd banking-ledger-api
make ci-deps-up
./mvnw -DskipTests validate
./mvnw test
./mvnw verify
make docker-build
make ci-deps-downSee docs/operations/QualityReport.md and docs/operations/BranchProtection.md for coverage artifacts, known test gaps, vulnerability thresholds, and recommended required checks.
Convenience commands are available from banking-ledger-api/:
make deps-up
make run
make test
make token-customer
make token-opsSee docs/operations/LocalDevelopment.md for the full command list, environment variables, seed data, and troubleshooting.
The fastest way to review the backend is to run the local services, start the API, then execute banking-ledger-api/http/demo-flow.http from an IDE REST client such as IntelliJ HTTP Client or VS Code REST Client.
From banking-ledger-api/, start dependencies and the API:
make deps-up
make runIn another terminal, confirm the API is healthy and issue sample role tokens if you want to test with curl manually:
curl http://localhost:8080/actuator/health
make token-customer
make token-teller
make token-ops
make token-auditorFor the full guided flow, open http/demo-flow.http and run the requests from top to bottom. The file first issues dev JWTs, then demonstrates:
- Seeded account lookup.
- Account creation as a teller.
- Transfer creation with
Idempotency-Key. - Idempotent replay using the same request.
- Idempotency conflict using the same key with a different body.
- Transfer lookup and reversal.
- Operational adjustment posting.
- Reconciliation batch import with a mismatch.
- Reconciliation result, ledger investigation, audit, and outbox queries.
The demo uses deterministic seed data loaded by the dev profile, including customer 00000000-0000-0000-0000-000000000001, account 00000000-0000-0000-0000-000000001001, and seeded transfer 00000000-0000-0000-0000-000000005001. If the database state becomes stale during repeated demos, reset it and start again:
make deps-reset
make deps-up
make runThe default profile is dev.
Profile config files:
src/main/resources/application.yamlsrc/main/resources/application-dev.yamlsrc/main/resources/application-ci.yamlsrc/main/resources/application-prod.yaml
Run with an explicit profile:
SPRING_PROFILES_ACTIVE=dev ./mvnw spring-boot:runFor production:
SPRING_PROFILES_ACTIVE=prod ./mvnw spring-boot:runThe prod profile requires real environment variables for database and Kafka configuration.
CI runs with SPRING_PROFILES_ACTIVE=ci. That profile uses Oracle Free and Kafka like development, but it only runs the core Flyway migrations and does not load the repeatable demo seed data from db/dev-migration.
Development Oracle container:
- Image:
gvenzl/oracle-free:23-slim-faststart - Port:
1521 - Service name:
FREEPDB1 - App user:
ledger_dev - App password:
ledger_dev_password - Admin password:
oracle_admin_password
JDBC URL from the host machine:
jdbc:oracle:thin:@localhost:1521/FREEPDB1
JDBC URL from another Docker container on the same compose network:
jdbc:oracle:thin:@oracle:1521/FREEPDB1
CloudBeaver runs in the browser at:
http://localhost:8978
Use these connection values inside CloudBeaver:
Host: oracle
Port: 1521
Service name: FREEPDB1
Username: ledger_dev
Password: ledger_dev_password
Use oracle as the host because CloudBeaver runs inside Docker on the same compose network as the database.
Development Kafka bootstrap server from the host machine:
localhost:9092
From another Docker container on the same compose network:
kafka:9092
Topic auto-creation is disabled. Create topics explicitly when event publishing is implemented.
The development and production compose files include a one-shot kafka-init service that creates these topics:
banking-ledger.ledger-eventsbanking-ledger.account-eventsbanking-ledger.reconciliation-events
The production compose file builds and runs the API container with Oracle and Kafka.
Create an environment file first:
cd banking-ledger-api
cp .env.example .envEdit .env and replace development passwords before running production services.
Start production services:
docker compose -f compose.prod.yaml up -d --buildStart production services with CloudBeaver enabled:
docker compose -f compose.prod.yaml --profile tools up -d --buildStop production services:
docker compose -f compose.prod.yaml downValidate the Maven project:
cd banking-ledger-api
./mvnw -DskipTests validateRun tests:
./mvnw testBuild the application:
./mvnw clean packageBuild the production Docker image through compose:
docker compose -f compose.prod.yaml build apiFlyway is enabled and looks for migrations in:
banking-ledger-api/src/main/resources/db/migration
Use names like:
V1__create_core_ledger_tables.sql
V2__create_idempotency_and_outbox_tables.sql
Keep migrations Oracle-compatible because the project is designed around Oracle and PL/SQL-style reporting.
See docs/backend/CoreBusinessLogic.md for the business and accounting flow, and docs/backend/DatabaseDesign.md for schema rationale, transaction isolation choices, locking strategy, and database best practices.
- Documentation index
- API contracts and examples
- Backend roadmap
- Quality report
- Local development guide
- Reports
- Architecture diagram
- Transfer flow diagram
- Reversal flow diagram
- Outbox publishing diagram
- Transaction boundaries diagram
- ERD
Key ADRs:
- Double-entry, amounts, and idempotency
- Transaction isolation and account locking
- Immutable reversals and adjustments
- JWT authentication and authorization
- Audit trail and investigation APIs
- Outbox and Kafka publishing
- Reconciliation design
- Developer experience
- Keep posted ledger records immutable.
- Use reversals or adjustment entries instead of destructive updates.
- Keep DTOs separate from JPA entities.
- Use explicit transaction boundaries around posting, reversal, and transfer flows.
- Store monetary amounts as integer minor units with explicit currency codes.
- Do not log sensitive data.
- Add integration tests for database constraints, rollback behavior, idempotency, and concurrent transfers.