Utility Account System

A production-grade billing system with a management UI and an open payment API for third-party providers. The engineering foundation every other project in this portfolio builds on.

Why this exists

Before integrating any payment vendors or building out my payment infrastructure, I needed a production-grade Spring Boot baseline I could trust, something that proved the patterns before applying them at scale. The Utility Account System is that foundation.

It's a billing system: it manages accounts, tracks balances, and processes transactions. It exposes an open payment API that third-party providers plug into directly, and a management UI for internal operations.

Stack

Built with a modern Java stack, production patterns throughout.

Java 21 Spring Boot 3.5 Spring Security Spring Data JPA Hibernate Liquibase HikariCP MapStruct Lombok Angular 19 PostgreSQL Testcontainers JUnit 5 Mockito Docker

Observability

Every payment and validation request is traceable end-to-end via Grafana. The dashboard below shows live payment and validation throughput, latency, and database connection pool health.

Utility Account service health Grafana dashboard

Live dashboard — current traffic from continuous soak testing.

258 Passing tests
196/s Peak throughput (load tested)
84ms p95 latency under load
0 Errors during load test

Security: two consumers, two models

The system has two distinct consumers with completely different security needs, handled by two independent Spring Security filter chains.

Management UI: JWT (Cookie + Bearer fallback), CSRF, CORS

Internal users authenticate via POST /api/auth/login. The JWT is delivered as an HTTP-only cookie for the Angular app. Full CSRF protection is enforced on all mutating requests via the XSRF-TOKEN cookie and Spring Security's CookieCsrfTokenRepository. CORS is locked to the Angular origin only. Bearer mode is supported as a fallback for Swagger UI and API tooling; the cookie takes priority when both are present.

Payment API: SHA-256 API key

Third-party payment providers call the payment API directly using SHA-256 hashed API keys via the X-Api-Key header. No browser session, no cookie. Each provider has its own key, its own permissions, and its own audit trail.

Try the project

Everything is live. Start with the management UI, explore the API, or trigger a real payment, all from the browser.

1. Open the management UI

Log in with admin / admin. From here you can browse customers, accounts, balances, and payment history.

2. Explore the API in Swagger

The UI uses the same REST API and you can explore it directly in Swagger. Open Swagger UI and select Admin API from the Select a definition dropdown (top right). Call POST /api/auth/login with admin / admin (the X-Auth-Mode: bearer header is pre-filled). Copy the accessToken from the response, then click Authorize (lock icon, top right) and paste it as a Bearer token.

3. Make a payment

Grab a customer or account number from the UI or API. Switch to Payments API in the Select a definition dropdown (top right). Click Authorize (lock icon, top right) and enter the MTN MoMo test key: 0df87c1d-2dde-4a08-8f9f-7739b471073a. Trigger a payment and watch the balance update in the UI.

Engineering decisions

Modern Java stack

Java 21 with virtual threads, Spring Boot 3.5, Spring Security, Spring Data JPA, Lombok, MapStruct, and Resilience4j. Clean layered architecture with strict separation between controllers, services, and repositories.

Role-based access control

Two roles: ROLE_ADMIN and ROLE_OPERATOR. Fine-grained access rules enforced via @PreAuthorize on controllers, keeping the rules close to the code they protect.

Idempotent payments

Every payment is keyed on a client-supplied paymentReference. Duplicate submissions return the original result without re-processing. Receipt IDs use UUID v7, giving time-ordered sequential IDs that index efficiently in PostgreSQL. Balance updates are atomic via database-level constraints, not application-level locks.

Liquibase for schema management

All schema changes go through versioned Liquibase changesets, reviewable in pull requests, reversible, and safe for zero-downtime deployments. ddl-auto is set to validate in all environments.

Testcontainers for real database testing

The full test suite runs against a real PostgreSQL instance spun up per test run. No mocks, no in-memory shortcuts. 258 tests, all passing.

Soft delete for providers

Providers are never hard-deleted. A deactivate/reactivate pattern preserves audit history and allows safe recovery without data loss.

Global exception handler

A single @RestControllerAdvice handles all exceptions and returns structured validationErrors maps, giving API consumers consistent, machine-readable error responses across all endpoints.

Correlation ID filter

Every request is assigned a X-Correlation-Id at the Nginx boundary and propagated through the filter chain into logs and responses. Makes distributed request tracing straightforward across services.

Pagination on all list endpoints

All list and search endpoints use Spring Data Pageable. No unbounded queries, consistent page/size/sort contract across the entire API.