Skip to content

Commit 5989fc4

Browse files
Add project documentation and local infrastructure (#4)
1 parent b49cc5a commit 5989fc4

5 files changed

Lines changed: 478 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.idea
2+
.env
3+
.env.*
4+
!.env.example
5+
6+
# Generated build output
7+
target/
8+
9+
# Local workflow guidance
10+
GIT_WORKFLOW_CONVENTION.md

‎Dockerfile‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# syntax=docker/dockerfile:1
2+
3+
# ─── Stage 1: Dependency Cache ────────────────────────────────────────────────
4+
FROM eclipse-temurin:25-jdk-alpine AS dependencies
5+
6+
WORKDIR /app
7+
8+
# Copy root and all module POM files for precise dependency caching
9+
COPY pom.xml ./
10+
COPY payguard-proto/pom.xml ./payguard-proto/
11+
COPY payguard-api-gateway/pom.xml ./payguard-api-gateway/
12+
COPY payguard-wallet-service/pom.xml ./payguard-wallet-service/
13+
COPY payguard-loan-service/pom.xml ./payguard-loan-service/
14+
COPY payguard-collateral-service/pom.xml ./payguard-collateral-service/
15+
16+
COPY --chmod=0755 mvnw ./mvnw
17+
COPY .mvn/ .mvn/
18+
19+
# Download dependencies offline to leverage Docker layer caching
20+
RUN ./mvnw --batch-mode --no-transfer-progress dependency:go-offline -B || true
21+
22+
23+
# ─── Stage 2: Application Build ───────────────────────────────────────────────
24+
FROM dependencies AS builder
25+
26+
WORKDIR /app
27+
28+
# Copy source code and configurations for all modules
29+
COPY config/ ./config/
30+
COPY payguard-proto/ ./payguard-proto/
31+
COPY payguard-api-gateway/./payguard-api-gateway/
32+
COPY payguard-wallet-service/ ./payguard-wallet-service/
33+
COPY payguard-loan-service/ ./payguard-loan-service/
34+
COPY payguard-collateral-service/ ./payguard-collateral-service/
35+
36+
# Receive service name via Build Arg (defaults to wallet service)
37+
ARG SERVICE_NAME=payguard-wallet-service
38+
39+
# Build the target service package along with internal dependencies (-am) without running tests
40+
RUN ./mvnw --batch-mode --no-transfer-progress clean package -pl :${SERVICE_NAME} -am -DskipTests
41+
42+
43+
# ─── Stage 3: Runtime ─────────────────────────────────────────────────────────
44+
FROM eclipse-temurin:25-jre-alpine AS runtime
45+
46+
WORKDIR /app
47+
48+
# High security: Create a non-root system user and isolate the data directory
49+
RUN addgroup -S payguard \
50+
&& adduser -S payguard -G payguard \
51+
&& mkdir -p /var/lib/payguard \
52+
&& chown -R payguard:payguard /var/lib/payguard
53+
54+
ARG SERVICE_NAME=payguard-wallet-service
55+
56+
# Copy the output JAR file from the build stage to the lightweight JRE runtime image
57+
COPY --from=builder --chown=payguard:payguard /app/${SERVICE_NAME}/target/*.jar ./app.jar
58+
59+
# Run the container as a non-privileged user
60+
USER payguard
61+
62+
EXPOSE 8080
63+
64+
# Java 25 runtime optimizations
65+
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"]

‎README.md‎

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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+
[![CI](https://img.shields.io/badge/CI-GitHub%20Actions-2088FF?style=flat-square&logo=githubactions&logoColor=white)](#)
8+
[![Java](https://img.shields.io/badge/Java-25-orange?style=flat-square&logo=openjdk)](https://openjdk.org)
9+
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.1-brightgreen?style=flat-square&logo=springboot)](https://spring.io/projects/spring-boot)
10+
[![Architecture](https://img.shields.io/badge/Architecture-Hexagonal%20%7C%20DDD-blueviolet?style=flat-square)](https://alistair.cockburn.us/hexagonal-architecture/)
11+
[![License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](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

Comments
 (0)