|
| 1 | +<div align="center"> |
| 2 | + |
| 3 | +# PayGuard |
| 4 | + |
| 5 | +**A modular fintech backend for wallet ledgering, loan origination, and digital-collateral risk management.** |
| 6 | + |
| 7 | +[](#) |
| 8 | +[](https://openjdk.org) |
| 9 | +[](https://spring.io/projects/spring-boot) |
| 10 | +[](https://alistair.cockburn.us/hexagonal-architecture/) |
| 11 | +[](LICENSE) |
| 12 | + |
| 13 | +[Overview](#overview) • |
| 14 | +[Services](#services) • |
| 15 | +[Architecture](#architecture) • |
| 16 | +[Key Engineering Decisions](#key-engineering-decisions) • |
| 17 | +[Getting Started](#getting-started) • |
| 18 | +[API Documentation](#api-documentation) • |
| 19 | +[Roadmap](#roadmap) |
| 20 | + |
| 21 | +</div> |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Overview |
| 26 | + |
| 27 | +PayGuard is a backend system for a single, coherent financial story: a customer locks digital assets as collateral, borrows against them, and pays the loan back — while the system continuously monitors the collateral's value and protects itself automatically if that value falls too far. |
| 28 | + |
| 29 | +The project is built as a set of **independently deployable microservices**, each owning its own database and communicating only through explicit contracts — never through a shared schema. It is a deliberate engineering exercise in the domain problems that sit underneath every regulated lending product: |
| 30 | + |
| 31 | +- Double-entry bookkeeping and derived, never-stored balances |
| 32 | +- Loan amortization, repayment waterfalls, and delinquency state machines |
| 33 | +- Loan-to-Value (LTV) risk monitoring, margin calls, and liquidation |
| 34 | +- Idempotent financial operations under concurrent access |
| 35 | +- Service-to-service communication boundaries (REST at the edge, gRPC where latency matters) |
| 36 | + |
| 37 | +It is not a payment-card switch or a full core-banking platform. The scope is intentionally narrow so that every part of it can be built, understood, and defended in depth. |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Core Capabilities |
| 42 | + |
| 43 | +### Implemented / In Progress |
| 44 | + |
| 45 | +- [ ] Account creation and double-entry ledger posting (`wallet-service`) |
| 46 | +- [ ] Hold → capture → release flow for provisional balance reservations |
| 47 | +- [ ] Loan application, approval, and amortization-schedule generation (`loan-service`) |
| 48 | +- [ ] Repayment waterfall (fees → interest → principal) |
| 49 | +- [ ] Collateral locking and Loan-to-Value calculation (`collateral-service`) |
| 50 | +- [ ] Margin-call and liquidation risk monitoring |
| 51 | +- [ ] API Gateway routing (REST-in, gRPC-out to internal services) |
| 52 | + |
| 53 | +### Planned |
| 54 | + |
| 55 | +- [ ] Kafka-based domain events between services (e.g. loan approval → collateral linkage) |
| 56 | +- [ ] Full CI pipeline (build, test, static analysis) via GitHub Actions |
| 57 | +- [ ] Expanded integration and concurrency test coverage |
| 58 | + |
| 59 | +> This README describes the target architecture and is kept in sync with implementation status via the checkboxes above — see [Current Limitations](#current-limitations) for what is intentionally out of scope for now. |
| 60 | +
|
| 61 | +--- |
| 62 | + |
| 63 | +## Services |
| 64 | + |
| 65 | +| Service | Responsibility | Persistence | Current State | |
| 66 | +|---|---|---|---| |
| 67 | +| `wallet-service` | Account balances, double-entry ledger, holds/captures | PostgreSQL/Oracle via **jOOQ** (append-only, no ORM overhead) | In development | |
| 68 | +| `loan-service` | Loan lifecycle, amortization schedules, repayments | Oracle via JPA/Hibernate | In development | |
| 69 | +| `collateral-service` | Collateral locking, price valuation, LTV monitoring, liquidation | Oracle via JPA/Hibernate | In development | |
| 70 | +| `api-gateway` | Single entry point; REST from clients, gRPC to internal services | — | Planned | |
| 71 | + |
| 72 | +> `wallet-service` deliberately uses jOOQ instead of JPA: a ledger is append-only, and jOOQ gives explicit control over `INSERT`-only SQL with no risk of an accidental `UPDATE` slipping in through dirty-checking. `loan-service` and `collateral-service` manage mutable, CRUD-shaped state, where JPA is a better fit. |
| 73 | +
|
| 74 | +--- |
| 75 | + |
| 76 | +## Architecture |
| 77 | + |
| 78 | +### System Overview |
| 79 | + |
| 80 | +```mermaid |
| 81 | +flowchart LR |
| 82 | + client["Client (REST)"] |
| 83 | + gateway["API Gateway<br/>(REST in, gRPC out)"] |
| 84 | + wallet["wallet-service<br/>(Ledger)"] |
| 85 | + loan["loan-service"] |
| 86 | + collateral["collateral-service"] |
| 87 | + oracle["Oracle 23c"] |
| 88 | + redis["Redis 7<br/>(price cache, distributed locks)"] |
| 89 | + kafka["Kafka (KRaft)<br/>domain events"] |
| 90 | +
|
| 91 | + client --> gateway |
| 92 | + gateway -->|gRPC| wallet |
| 93 | + gateway -->|gRPC| loan |
| 94 | + gateway -->|gRPC| collateral |
| 95 | +
|
| 96 | + loan -->|REST: disburse/repay| wallet |
| 97 | + loan -->|REST: check LTV| collateral |
| 98 | + collateral -->|REST: outstanding balance| loan |
| 99 | + collateral --> redis |
| 100 | +
|
| 101 | + wallet --> oracle |
| 102 | + loan --> oracle |
| 103 | + collateral --> oracle |
| 104 | +
|
| 105 | + loan -.->|planned| kafka |
| 106 | + collateral -.->|planned| kafka |
| 107 | +``` |
| 108 | + |
| 109 | +### Hexagonal Structure (per service) |
| 110 | + |
| 111 | +```mermaid |
| 112 | +flowchart LR |
| 113 | + Inbound["Inbound Adapters<br/>REST Controllers"] |
| 114 | + Application["Application Layer<br/>Use-Case Services"] |
| 115 | + Domain["Domain Layer<br/>Aggregates / Value Objects / Invariants"] |
| 116 | + Ports["Outbound Ports"] |
| 117 | + Outbound["Outbound Adapters<br/>jOOQ / JPA / HTTP Clients"] |
| 118 | +
|
| 119 | + Inbound --> Application |
| 120 | + Application --> Domain |
| 121 | + Application --> Ports |
| 122 | + Outbound --> Ports |
| 123 | +``` |
| 124 | + |
| 125 | +### Dependency Rules |
| 126 | + |
| 127 | +- Domain code has no Spring, persistence, or HTTP dependencies — invariants (e.g. debit = credit) are testable without a running application. |
| 128 | +- Each service owns its own database schema; no service queries another's tables directly. |
| 129 | +- Cross-service calls happen only through the declared client ports (`WalletServiceClient`, `LoanServiceClient`, `CollateralServiceClient`), each with a REST adapter today and a gRPC adapter available for the gateway path. |
| 130 | +- Dependencies point inward, toward the domain core. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Key Engineering Decisions |
| 135 | + |
| 136 | +### No stored balance column |
| 137 | + |
| 138 | +`wallet-service` never persists a mutable `balance` field. Every balance is derived from the sum of immutable `ledger_entry` rows. This eliminates an entire class of race conditions that come from concurrent `UPDATE balance = balance - x` statements, at the cost of computing balances on read — a trade-off documented here rather than hidden. |
| 139 | + |
| 140 | +### Holds are not ledger entries |
| 141 | + |
| 142 | +A hold (reservation) reduces *available* balance without touching the *ledger* balance. Only capture — the actual settlement of a hold — creates real, immutable ledger entries. This mirrors the authorize-then-capture pattern used throughout the payments industry. |
| 143 | + |
| 144 | +### Concurrency: inserts don't need locks, holds do |
| 145 | + |
| 146 | +Posting a ledger transaction is a pure `INSERT` and needs no row locking. Creating a *hold* does need one (`SELECT ... FOR UPDATE` or a Redis-based distributed lock), because two concurrent hold requests could otherwise both read a stale available balance and jointly overdraw the account. |
| 147 | + |
| 148 | +### gRPC only at the gateway boundary |
| 149 | + |
| 150 | +Internal service-to-service calls use plain REST. gRPC is used exclusively between the API Gateway and the internal services, where the latency and schema-contract benefits are worth the added operational complexity — not applied uniformly out of habit. |
| 151 | + |
| 152 | +### Derived, never-stored LTV |
| 153 | + |
| 154 | +Loan-to-Value is calculated on demand from the latest price snapshot and the loan's current outstanding principal, never cached as a stored "current LTV" field. This keeps the number provably correct at the moment it's checked, which matters more here than raw read speed. |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Getting Started |
| 159 | + |
| 160 | +### Prerequisites |
| 161 | + |
| 162 | +- Java 25 or newer |
| 163 | +- Docker with Docker Compose |
| 164 | +- Maven Wrapper (included, no separate Maven install required) |
| 165 | + |
| 166 | +```bash |
| 167 | +java -version |
| 168 | +docker --version |
| 169 | +docker compose version |
| 170 | +``` |
| 171 | + |
| 172 | +### 1. Clone the Repository |
| 173 | + |
| 174 | +```bash |
| 175 | +git clone https://github.com/<your-org>/payguard.git |
| 176 | +cd payguard |
| 177 | +``` |
| 178 | + |
| 179 | +### 2. Configure the Environment |
| 180 | + |
| 181 | +Create a `.env` file in the repository root: |
| 182 | + |
| 183 | +```env |
| 184 | +# Oracle |
| 185 | +ORACLE_IMAGE=gvenzl/oracle-free:23-slim-faststart |
| 186 | +ORACLE_HOST_PORT=1521 |
| 187 | +ORACLE_PASSWORD=change-me-for-local-development |
| 188 | +
|
| 189 | +# Redis |
| 190 | +REDIS_IMAGE=redis:7-alpine |
| 191 | +REDIS_HOST_PORT=6379 |
| 192 | +
|
| 193 | +# Kafka (KRaft mode — no Zookeeper) |
| 194 | +KAFKA_CLUSTER_ID=replace-with-a-generated-cluster-id |
| 195 | +
|
| 196 | +# Per-service ports |
| 197 | +WALLET_SERVICE_PORT=8081 |
| 198 | +LOAN_SERVICE_PORT=8082 |
| 199 | +COLLATERAL_SERVICE_PORT=8083 |
| 200 | +API_GATEWAY_PORT=8080 |
| 201 | +``` |
| 202 | + |
| 203 | +> Never commit real credentials. The values above are placeholders for local development. |
| 204 | +
|
| 205 | +### 3. Start Infrastructure |
| 206 | + |
| 207 | +```bash |
| 208 | +docker compose up -d payguard-oracle payguard-redis payguard-kafka --wait |
| 209 | +``` |
| 210 | + |
| 211 | +### 4. Run a Service |
| 212 | + |
| 213 | +```bash |
| 214 | +./mvnw spring-boot:run -pl payguard-wallet-service |
| 215 | +``` |
| 216 | + |
| 217 | +### 5. Open the API Documentation |
| 218 | + |
| 219 | +```text |
| 220 | +http://localhost:8081/swagger-ui/index.html |
| 221 | +``` |
| 222 | + |
| 223 | +To stop the infrastructure: |
| 224 | + |
| 225 | +```bash |
| 226 | +docker compose down |
| 227 | +``` |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## API Documentation |
| 232 | + |
| 233 | +Each service exposes its own OpenAPI specification and Swagger UI at `/swagger-ui/index.html`. Representative endpoints: |
| 234 | + |
| 235 | +| Service | Method | Path | Purpose | |
| 236 | +|---|---|---|---| |
| 237 | +| wallet-service | `POST` | `/accounts/{id}/holds` | Reserve funds ahead of a capture | |
| 238 | +| wallet-service | `GET` | `/accounts/{id}/balance` | Ledger and available balance | |
| 239 | +| loan-service | `POST` | `/loans/{id}/approve` | Generate the amortization schedule | |
| 240 | +| loan-service | `POST` | `/loans/{id}/repayments` | Apply a payment through the waterfall | |
| 241 | +| collateral-service | `GET` | `/collateral/{id}` | Current position and live LTV | |
| 242 | +| collateral-service | `POST` | `/collateral/refresh-prices` | Re-evaluate risk against latest prices | |
| 243 | + |
| 244 | +The OpenAPI document for each service is the source of truth for exact request/response schemas. |
| 245 | + |
| 246 | +--- |
| 247 | + |
| 248 | +## Code Quality |
| 249 | + |
| 250 | +| Check | Tool | Execution | |
| 251 | +|---|---|---| |
| 252 | +| Build & tests | Maven | CI and local | |
| 253 | +| Formatting / static analysis | *(planned)* | CI | |
| 254 | + |
| 255 | +```bash |
| 256 | +./mvnw clean verify |
| 257 | +``` |
| 258 | + |
| 259 | +Install the local Git hook so every `git push` runs the same verification first: |
| 260 | + |
| 261 | +```bash |
| 262 | +./mvnw -Pinstall-git-hooks initialize |
| 263 | +``` |
| 264 | + |
| 265 | +--- |
| 266 | + |
| 267 | +## Roadmap |
| 268 | + |
| 269 | +### wallet-service |
| 270 | +- [ ] Account CRUD |
| 271 | +- [ ] Direct transfer flow (Flow A) |
| 272 | +- [ ] Hold → capture/release flow (Flow B) |
| 273 | +- [ ] Idempotency-key enforcement |
| 274 | + |
| 275 | +### loan-service |
| 276 | +- [ ] Loan application and state machine |
| 277 | +- [ ] Amortization schedule generation |
| 278 | +- [ ] Repayment waterfall |
| 279 | +- [ ] Delinquency/default detection |
| 280 | + |
| 281 | +### collateral-service |
| 282 | +- [ ] Collateral locking |
| 283 | +- [ ] Price-oracle integration |
| 284 | +- [ ] LTV monitoring and margin calls |
| 285 | +- [ ] Liquidation flow |
| 286 | + |
| 287 | +### Platform |
| 288 | +- [ ] API Gateway (REST → gRPC) |
| 289 | +- [ ] Kafka domain events |
| 290 | +- [ ] Full CI via GitHub Actions |
| 291 | + |
| 292 | +--- |
| 293 | + |
| 294 | +## Current Limitations |
| 295 | + |
| 296 | +- PayGuard is an engineering-demonstration project, not a production lending platform. |
| 297 | +- No real price feed, KYC, or payment-network integration is connected; these are simulated or stubbed. |
| 298 | +- Continuous deployment is out of scope for the current phase — the focus is on correctness and depth of the core domain services. |
| 299 | +- Each service currently assumes a single instance; distributed-lock behavior for holds is documented but not yet load-tested at scale. |
| 300 | + |
| 301 | +--- |
| 302 | + |
| 303 | +## License |
| 304 | + |
| 305 | +This project is licensed under the [MIT License](LICENSE). |
0 commit comments