Skip to content

Repository files navigation

Banking Transaction API

Java Spring Boot PostgreSQL Apache Kafka Docker License

A REST API for managing bank accounts and money transfers built with Spring Boot.


Why I built this

I wanted to go beyond basic CRUD and tackle something that actually breaks in production — concurrent balance updates. When two clients transfer money at the same time, race conditions can corrupt balances or cause double-spend issues. That's a real problem that needs a real solution, not just a @PutMapping.

So I built this to explore a few things specifically:

  • How pessimistic locking prevents concurrent write conflicts at the DB level
  • How Kafka fits into a transactional flow (spoiler: it's not magic, ordering matters)
  • Liquibase over ddl-auto=update — something I want to use properly in team environments

The result is a small but realistic banking backend where transfers are atomic, events are asynchronous, and the schema is versioned.


How it works

flowchart TB
    subgraph Client
        UI[HTTP Client / Swagger UI]
    end

    subgraph API["Transaction API (Spring Boot)"]
        C[Controllers]
        S[Services]
        R[Repositories]
        D[Domain Entities]
        C --> S --> R --> D
    end

    subgraph Security
        JWT[JwtFilter + JwtUtil]
        UDS[UserDetailsServiceImpl]
    end

    subgraph Messaging
        KP[TransactionCreatedEventProducer]
        KC[TransactionCreatedEventConsumer]
    end

    UI -->|REST + Bearer JWT| C
    C --> JWT
    JWT --> UDS
    S -->|after transfer| KP
    KP -->|publish| KF[(Kafka)]
    KF -->|consume| KC
    KC -->|update status| R
    R --> PG[(PostgreSQL)]
Loading

Transfer flow step by step:

1. POST /transfer comes in
2. Both accounts locked in ID order (prevents deadlocks)
3. Balances updated inside a single @Transactional block
4. Transaction saved as PENDING
5. Event published to Kafka
6. Consumer picks it up → logs details → marks transaction COMPLETED

The PENDING → COMPLETED lifecycle is intentional — it makes the async nature of the flow visible and easier to debug.


Stack

Language Java 21
Framework Spring Boot 3.5 — Web, Security, Data JPA, Validation, Actuator
Database PostgreSQL 16
Migrations Liquibase
Messaging Apache Kafka 7.6 + Zookeeper
Auth Spring Security + JWT (jjwt)
Docs SpringDoc OpenAPI / Swagger UI
Build Maven
Infra Docker + Docker Compose

Running locally

You need Docker and Docker Compose v2. That's it.

git clone <repository-url>
cd transaction-api
cp .env.example .env   # Windows: copy .env.example .env
docker compose up --build

This starts PostgreSQL, Zookeeper, Kafka, and the app — in the right order with health checks.

URL
API http://localhost:8080
Swagger UI http://localhost:8080/swagger-ui.html
Health http://localhost:8080/actuator/health

Quick test after startup:

# Register
curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password123"}'

# Login → copy the token from response
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password123"}'

# Create account
curl -X POST http://localhost:8080/api/accounts \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ownerId":1,"currency":"USD"}'

To stop:

docker compose down       # stop
docker compose down -v    # stop + wipe volumes

If you want to run the app outside Docker but keep the infrastructure:

docker compose up postgres zookeeper kafka -d
./mvnw spring-boot:run

API

All endpoints except auth require Authorization: Bearer <token>.

Auth

Method Path Description
POST /api/auth/register Create account, returns JWT
POST /api/auth/login Login, returns JWT

Accounts

Method Path Description
POST /api/accounts Create account
GET /api/accounts/{id} Get by ID
GET /api/accounts/owner/{ownerId} Get all accounts for a user

Transactions

Method Path Description
POST /api/transactions/accounts/{id}/deposit Deposit
POST /api/transactions/accounts/{id}/withdraw Withdraw
POST /api/transactions/accounts/{id}/transfer Transfer (async via Kafka)
GET /api/transactions History — filter by accountId, type, status, from, to

Admin

Method Path Description
GET /api/admin/accounts All accounts (ROLE_ADMIN only)

Config

The app works out of the box with Docker Compose. If running standalone, these env vars matter:

Copy .env.example to .env before running Docker Compose. Secrets stay in .env (gitignored); Docker loads them via env_file.

Variable Default Notes
APP_JWT_SECRET Required. Minimum 32 characters
SPRING_DATASOURCE_PASSWORD postgres DB password
POSTGRES_DB transaction_db Database name
SPRING_DATASOURCE_USERNAME postgres DB user
SPRING_LOCAL_PORT / SPRING_DOCKER_PORT 8080 Host and container port for the app
POSTGRES_LOCAL_PORT / POSTGRES_DOCKER_PORT 5432 Host and container port for PostgreSQL
APP_JWT_EXPIRATION_MS 3600000 Token TTL in ms
SPRING_DATASOURCE_URL jdbc:postgresql://localhost:5432/transaction_db Local run only; Docker overrides to postgres host
SPRING_KAFKA_BOOTSTRAP_SERVERS localhost:29092 Local run only; Docker overrides to kafka:9092

Project layout

src/main/java/com/banking/transaction/
├── controller/     REST endpoints
├── service/        business logic, locking, transactions
├── repository/     JPA
├── domain/         entities and enums
├── dto/            request/response shapes
├── mapper/         entity ↔ DTO
├── security/       JWT filter and token utils
├── kafka/          producer and consumer
├── config/         Spring config
└── exception/      global error handler

src/main/resources/
├── application.yml
└── db/changelog/   Liquibase migrations

License

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages