Fintech Transaction API
Build LedgerAPI — a double-entry ledger with per-transaction policy enforcement, amount-based approval workflows, SOC 2 audit trail, and idempotency. Java + Spring Boot.
Fintech Transaction API
What you'll build: LedgerAPI — a double-entry ledger service for a fintech platform. Every
debit/credit goes through a policy check before execution. Transactions over $10,000 require a
compliance_approved claim. Failed auth attempts are rate-limited. The full audit trail is
SOC 2 compliant. Java + Spring Boot.
The Story
Clearbridge Financial processes inter-bank transfers for SMEs. Regulatory requirements:
- Every transaction authenticated via signed JWT (bank's IdP)
- Transactions > $10K require two-factor + compliance officer approval (claim in JWT)
viewerrole can read ledger — never writeoperatorrole initiates transfers, blocked above threshold without approvalcompliancerole can read everything including suspicious flag metadata- All mutations immutable in audit log — no soft deletes, no overwrites
Without CoreSDK: policy checks are if amount > threshold && hasRole(...) scattered across
service classes. With CoreSDK: one @PreAuthorize-style check backed by Rego.
Architecture
Bank Operator / Compliance Officer
│ JWT (role + mfa_verified + compliance_approved)
▼
┌──────────────────────────────────────┐
│ Spring Boot API │
│ │
│ CoreSDKFilter (auth on all routes) │ ← validates JWT via sidecar
│ │ │
│ @RequireRole("operator") │
│ │ │
│ Amount policy check │ ← Rego: amount > 10000 needs approval
│ │ │
│ Idempotency key dedup │ ← prevents double-spend
│ │ │
│ Double-entry ledger write │
│ │ │
│ SOC 2 audit append │
└──────────────────────────────────────┘
│ gRPC :50051
▼
┌─────────────────┐
│ CoreSDK Sidecar│
└─────────────────┘Prerequisites
<!-- pom.xml -->
<dependency>
<groupId>io.coresdk</groupId>
<artifactId>coresdk-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Quickstart
git clone https://github.com/coresdk-dev/examples
cd examples/java/ledgerapi
CORESDK_FAIL_MODE=closed mvn spring-boot:runCode Walkthrough
Step 1 — Spring Boot autoconfiguration
application.yml:
coresdk:
sidecar-addr: localhost:50051
tenant-id: clearbridge-financial
fail-mode: closed # never fail-open for financial transactions
service-name: ledger-api
spring:
application:
name: ledger-apiThe CoreSDKFilter is auto-registered — all routes protected by default.
@SpringBootApplication
public class LedgerApiApplication {
public static void main(String[] args) {
SpringApplication.run(LedgerApiApplication.class, args);
}
}Step 2 — Transaction request with policy enforcement
@RestController
@RequestMapping("/api/v1/transactions")
public class TransactionController {
@Autowired private CoreSDK sdk;
@Autowired private LedgerService ledger;
@Autowired private AuditService audit;
@PostMapping
public ResponseEntity<?> createTransaction(
@RequestBody TransactionRequest req,
@RequestHeader("Authorization") String authHeader,
HttpServletRequest httpReq) {
// Claims already validated by CoreSDKFilter
Claims claims = (Claims) httpReq.getAttribute("coresdk.claims");
// Role check: operator minimum
if (!claims.getRoles().contains("operator")) {
return problem(403, "Forbidden",
"Role 'operator' required to initiate transactions.");
}
// Policy check: high-value transactions need compliance approval
boolean allowed = sdk.evaluatePolicy("ledger.allow_transaction", Map.of(
"amount", req.getAmountCents(),
"currency", req.getCurrency(),
"roles", claims.getRoles(),
"compliance_approved", claims.hasAttribute("compliance_approved"),
"mfa_verified", claims.hasAttribute("mfa_verified")
)).join().isAllowed();
if (!allowed) {
audit.record(claims, "transaction.denied", req, "policy_rejected");
return problem(403, "Compliance Hold",
req.getAmountCents() > 1_000_000
? "Transactions over $10,000 require compliance officer approval."
: "Transaction rejected by policy.");
}
// Idempotency: reject duplicate submission
String idempotencyKey = req.getIdempotencyKey();
if (ledger.exists(idempotencyKey)) {
return ResponseEntity.ok(ledger.get(idempotencyKey)); // return existing
}
// Execute double-entry
Transaction tx = ledger.execute(req, claims.getSubject(), claims.getTenantId());
audit.record(claims, "transaction.created", req, "success");
return ResponseEntity.status(201).body(tx);
}
}Step 3 — Rego policy for transaction approval
policies/ledger.rego:
package ledger
import future.keywords.if
default allow_transaction := false
# Small transactions: operator role sufficient
allow_transaction if {
input.amount < 1_000_000 # < $10,000 in cents
"operator" in input.roles
input.mfa_verified == true
}
# Large transactions: require compliance approval claim in JWT
allow_transaction if {
input.amount >= 1_000_000
"operator" in input.roles
input.compliance_approved == true
input.mfa_verified == true
}
# Compliance officers can always read (but not write — separate rule)
allow_read if {
"compliance" in input.roles
}Step 4 — Double-entry ledger
@Service
public class LedgerService {
// In production: use a proper database with serializable transactions
private final Map<String, List<LedgerEntry>> accounts = new ConcurrentHashMap<>();
private final Map<String, Transaction> idempotencyStore = new ConcurrentHashMap<>();
public Transaction execute(TransactionRequest req, String actor, String tenantId) {
// Idempotency guard
if (idempotencyStore.containsKey(req.getIdempotencyKey())) {
return idempotencyStore.get(req.getIdempotencyKey());
}
String txId = UUID.randomUUID().toString();
// Debit source account
debit(req.getFromAccountId(), req.getAmountCents(), txId, tenantId);
// Credit destination account
credit(req.getToAccountId(), req.getAmountCents(), txId, tenantId);
Transaction tx = Transaction.builder()
.id(txId)
.fromAccount(req.getFromAccountId())
.toAccount(req.getToAccountId())
.amountCents(req.getAmountCents())
.currency(req.getCurrency())
.actor(actor)
.tenantId(tenantId)
.createdAt(Instant.now())
.build();
idempotencyStore.put(req.getIdempotencyKey(), tx);
return tx;
}
private void debit(String accountId, long amountCents, String txId, String tenantId) {
accounts.computeIfAbsent(accountId, k -> new ArrayList<>())
.add(new LedgerEntry(txId, tenantId, -amountCents, Instant.now()));
}
private void credit(String accountId, long amountCents, String txId, String tenantId) {
accounts.computeIfAbsent(accountId, k -> new ArrayList<>())
.add(new LedgerEntry(txId, tenantId, +amountCents, Instant.now()));
}
public long getBalance(String accountId, String tenantId) {
return accounts.getOrDefault(accountId, List.of()).stream()
.filter(e -> e.getTenantId().equals(tenantId))
.mapToLong(LedgerEntry::getAmountCents)
.sum();
}
}Step 5 — SOC 2 audit trail
@Service
public class AuditService {
// Append-only — no update/delete methods on this list
private final List<AuditRecord> log = Collections.synchronizedList(new ArrayList<>());
public void record(Claims claims, String event, Object subject, String outcome) {
log.add(AuditRecord.builder()
.timestamp(Instant.now())
.actor(claims.getSubject())
.tenantId(claims.getTenantId())
.roles(claims.getRoles())
.event(event)
.subjectType(subject.getClass().getSimpleName())
.outcome(outcome)
.build());
}
// Compliance officers can read audit log
public List<AuditRecord> getLog(String tenantId) {
return log.stream()
.filter(r -> r.getTenantId().equals(tenantId))
.toList();
}
}Test Scenarios
# Small transfer — operator + MFA: approved
curl -X POST http://localhost:8080/api/v1/transactions \
-H "Authorization: Bearer operator-mfa-token" \
-H "Content-Type: application/json" \
-d '{"from":"acct-001","to":"acct-002","amount_cents":50000,"currency":"USD","idempotency_key":"tx-001"}'
# 201 Created
# Large transfer — operator without compliance approval: denied
curl -X POST http://localhost:8080/api/v1/transactions \
-H "Authorization: Bearer operator-mfa-token" \
-d '{"from":"acct-001","to":"acct-002","amount_cents":2000000,"currency":"USD","idempotency_key":"tx-002"}'
# 403 {"detail":"Transactions over $10,000 require compliance officer approval."}
# Large transfer — with compliance_approved claim: approved
curl -X POST http://localhost:8080/api/v1/transactions \
-H "Authorization: Bearer compliance-approved-token" \
-d '{"from":"acct-001","to":"acct-002","amount_cents":2000000,"currency":"USD","idempotency_key":"tx-003"}'
# 201 Created
# Duplicate idempotency key: returns existing transaction (no double-spend)
curl -X POST http://localhost:8080/api/v1/transactions \
-H "Authorization: Bearer compliance-approved-token" \
-d '{"from":"acct-001","to":"acct-002","amount_cents":2000000,"currency":"USD","idempotency_key":"tx-003"}'
# 200 OK — same tx-003 returned, no second debitSOC 2 Compliance Notes
| Control | Implementation |
|---|---|
| CC6.1 — Logical access | CoreSDKFilter + role claims from IdP JWT |
| CC6.3 — Least privilege | Rego policy: operator can't bypass threshold without approval |
| CC7.2 — System monitoring | AuditService append-only log, compliance role can read |
| CC8.1 — Change management | Idempotency key prevents duplicate mutations |
| A1.2 — Availability | fail-mode=closed prevents auth bypass during outages |
Full Source
cd examples/java/ledgerapi
CORESDK_FAIL_MODE=closed mvn spring-boot:runHIPAA-Ready Healthcare API
Build MediRecord — a patient record API with role-based field masking, break-glass access, audit trails, and fail-closed enforcement. Go SDK + Gin.
SaaS Billing Webhooks with Tenant Scoping
Build SecurePay — a Stripe webhook processor that validates signatures, scopes events to tenants, gates feature access via feature flags, and emits structured audit trails. Python + FastAPI.