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.
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.
Live dashboard — current traffic from continuous soak testing.
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.