Backend Engineering Project

Digital Wallet Backend

Secure transaction workflows, built for correctness under concurrency.

Overview

The Digital Wallet Backend is a Java and Spring Boot service for wallet-based money movement. Users register and verify their account with an OTP, get a wallet, deposit funds, and transfer money to other users either immediately or on a schedule. Identity verification is handled through KYC — users submit a national ID, the backend runs OCR to extract their details, and verified users unlock the full feature set.

The interesting part is not the CRUD around it — it's what has to be true for a balance to be trustworthy: transactions that don't interleave badly, retries that don't duplicate money, actions that can be audited, and permissions that hold at the endpoint boundary. This is a learning-driven project built to work through those concerns properly rather than a deployed financial product.

Key features

  • User registration with OTP verification
  • Deposits
  • Money transfers between wallets
  • Scheduled transfers
  • Pessimistic locking for concurrent transactions
  • Idempotent transaction requests
  • WebSocket-based real-time notifications
  • Audit logs
  • Role-based access control for users and administrators
  • KYC verification with OCR-based national ID scanning

Engineering highlights

CONCURRENCY

Concurrency control

Balance changes are the one place where a lost update is unacceptable. Wallet rows are read with a pessimistic write lock inside the transaction that mutates them, so two concurrent transfers touching the same wallet serialise at the database instead of racing on stale balances.

IDEMPOTENCY

Idempotency

Clients retry — after a timeout, a dropped connection, or a double tap. Transaction endpoints accept an idempotency key so a repeated request resolves to the original transaction result rather than creating a second transfer.

SECURITY

Security

Registration is verified through OTP before an account becomes usable. Spring Security handles authentication and guards endpoints.

ACCESS CONTROL

Role-based access control

Two roles — user and admin — govern what each account can do. Users can create wallets, transfer funds, and report suspicious transactions. Admins oversee the platform and can reverse transactions that have been reported, ensuring disputes are resolved without direct database access.

REAL-TIME

Real-time communication

Once a transaction commits, the affected user is notified over a WebSocket connection instead of relying on the client to poll — useful for transfers that arrive while a session is open.

AUDITABILITY

Auditability

Transaction-relevant actions are written to audit logs, so the sequence of events behind a balance can be reconstructed after the fact rather than inferred from the current state.

SCHEDULING

Scheduled transfers

Transfers can be scheduled for later execution and are picked up by a scheduled job, which reuses the same locking and idempotency guarantees as an immediate transfer.

KYC

KYC verification with OCR

Users can submit a national ID for identity verification. The backend uses OCR to extract key details — name, date of birth, document number — from the uploaded image, auto-populates the user's profile fields, and records the verification status so KYC-gated operations can be enforced downstream.

Architecture

Client

HTTP

REST API

JSON

Spring Security

authn / RBAC

Controller

DTO mapping

Service Layer

transactions

Repository

Spring Data JPA

PostgreSQL

source of truth

Redis

supporting cache

WebSockets

notifications

Scheduler

scheduled transfers

OCR Service

KYC / ID scanning

A request travels from the client through the REST API, is authenticated and authorised by Spring Security, mapped in a controller, executed inside a transactional service, and persisted through repositories into PostgreSQL. Redis, the WebSocket channel and the scheduler sit alongside that path as supporting infrastructure.

Engineering decisions

Pessimistic over optimistic locking. Wallet contention is expected rather than rare, so retrying failed optimistic writes would add complexity for no benefit. Locking the row for the duration of the transaction keeps the critical section short and the outcome predictable.

Idempotency at the API boundary. Deduplicating at the entry point means retry safety doesn't have to be re-implemented inside every downstream operation.

Layered structure. Controllers stay thin and speak DTOs, services own transactional business rules, repositories own persistence. Testing with JUnit and Mockito is straightforward as a result.

Technology stack

  • Java
  • Spring Boot
  • Spring Security
  • Hibernate / JPA
  • PostgreSQL
  • Supabase
  • REST APIs
  • Redis
  • Docker
  • Git
  • Maven
  • JUnit / Mockito
  • Swagger / OpenAPI

Project links