๐ก๏ธ FraudShield
Real-time, event-driven fraud detection โ from raw payment event to user alert in under 200 ms.
What FraudShield Is
In one line: FraudShield watches every payment as it happens, decides within milliseconds whether it looks fraudulent, and instantly warns the user if something is wrong โ exactly like the fraud systems running behind banks, Stripe, PayPal, and Paytm.
The Problem It Solves
When you swipe a card or tap "pay," a bank has a fraction of a second to decide: is this really you, or is this a stolen card? If they are too slow, fraud goes through and the bank loses money. If they are too aggressive, they block real customers and everyone gets annoyed. Doing this well, at the scale of thousands of payments per second, is one of the hardest problems in financial software.
FraudShield is a production-grade real-time fraud detection system. It is a backend platform that receives payment events, scores each one against a set of fraud rules in milliseconds, and raises an alert the moment something looks suspicious.
What It Actually Does โ Step by Step
Imagine a customer named Priya. Here is what FraudShield does the instant a payment on her account comes in:
- 1Receives the payment. A merchant's app sends a transaction โ amount, currency, merchant, location โ to FraudShield's secure API. The request must carry a valid login token, or it is rejected on the spot.
- 2Checks for fraud patterns. Is Priya suddenly making 6 payments in a minute? Is this payment 50ร bigger than her usual spend? Is it coming from a country she has never shopped from before? Each suspicious signal adds to a "fraud score."
- 3Makes a decision. If the total score crosses a threshold, the payment is flagged. If not, it passes silently. This whole decision takes under a tenth of a second.
- 4Alerts the user. If flagged, Priya instantly gets a notification: "We noticed an unusual payment on your account." A human fraud analyst can also review it and override the decision.
- 5Records everything. Every payment, every decision, every analyst action is logged โ so there is a complete, tamper-evident audit trail.
Who Uses It
Three kinds of people interact with FraudShield: end users (customers like Priya who receive alerts), payment clients (merchant apps that submit transactions), and fraud analysts (staff who review flagged payments and tune the detection rules).
Architecture Overview
FraudShield is built with production patterns and best practices: event-driven architecture, circuit breakers, stateless JWT authentication, audit logging, and service discovery. Every component is designed to scale horizontally and handle real-time payment fraud detection at enterprise scale.
At a Glance
Here is the same thing in one technical sentence, for the quick summary: FraudShield is a production-grade, event-driven fraud detection and alerting platform that ingests payment transaction events, runs them through a configurable multi-rule scoring engine, flags suspicious activity, and notifies users โ all in near real-time.
Real-Time Detection
Kafka pipeline evaluates every transaction in under 100 ms P99.
Event-DrivenMulti-Layer Security
JWT + refresh token rotation, RBAC, token revocation, BCrypt password hashing.
Spring Security 7Dynamic Rules Engine
Fraud rules stored in DB, updated without redeployment. Velocity, amount anomaly, geo-mismatch.
Live ReloadResilience Built-In
Circuit breakers, retries, DLQ, idempotent processing โ nothing is lost.
Resilience4jFull Audit Trail
Every verdict, override, and rule change logged with actor and timestamp.
Audit LogEnterprise-Ready Local Dev
H2 in-memory DB. One command to run locally. Scales to PostgreSQL in production.
Docker ComposeArchitecture. FraudShield demonstrates production-grade fraud detection: deterministic scoring, stateless auth, horizontal scaling, event-driven processing, and comprehensive audit trails.
The Big Picture
Six Spring Boot microservices communicate through Apache Kafka and REST. The Transaction Service is the only public entry point. Everything else is internal. Redis underpins two cross-cutting concerns: sliding-window fraud counters and JWT token revocation.
High-Level Design
The formal architectural decisions and trade-offs โ the document an architect produces before deployment, following enterprise patterns for fraud detection systems.
3.1 Context & Scope
FraudShield sits between payment processors and end-users. It does not process payments โ it observes them and reacts.
| Actor | Role | Interface |
|---|---|---|
| Payment Client | Submits transaction events | POST /api/transactions + JWT |
| End User | Receives alerts, views history | GET /api/alerts + JWT |
| Fraud Analyst | Reviews flags, tunes rules | Admin API + ROLE_ANALYST |
| System Admin | Manages users, roles | Admin API + ROLE_ADMIN |
Out of scope: Payment processing, chargebacks. Card data is never stored โ only transaction metadata.
3.2 Architectural Style
Event-Driven Microservices with CQRS. Each service owns its data, communicates asynchronously via Kafka, scales independently.
| Decision | Choice | Rejected | Why |
|---|---|---|---|
| Inter-service comms | Kafka (async) | REST chains | Decoupling, replay, backpressure. Rules Engine down โ queue, no loss. |
| Read/write split | CQRS | Shared model | Ingestion and dashboards have different scaling needs. |
| Service boundaries | Per-capability (6) | Monolith | Rules Engine needs 6ร more instances than Admin API. |
| Authentication | JWT + Refresh | Server sessions | Stateless โ horizontal scaling, zero shared state. |
| Velocity tracking | Redis sliding window | SQL COUNT | Sub-ms vs ~5ms. At 50k TPS the gap is enormous. |
3.3 Container View (C4-L2)
3.4 Data Flows
| Topic | Producer | Consumer | Partitions | Retention |
|---|---|---|---|---|
raw-transactions | Transaction Svc | Rules Engine | 6 / 1 dev | 7 days |
fraud-verdicts | Rules Engine | Alert, Admin | 6 / 1 dev | 7 days |
alert-notifications | Alert Svc | Notification | 3 / 1 dev | 3 days |
raw-transactions.DLT | Spring Kafka | Ops team | 1 | 30 days |
Design note: Kafka partitions cap consumer parallelism. 6 partitions + 6 instances = 1 each. A 7th sits idle โ Kafka won't split a partition. This constraint drives scaling decisions.
3.5 Non-Functional Requirements
| Attribute | Target | How Achieved |
|---|---|---|
| Fraud eval latency | P99 < 100 ms | Redis counters, dedicated thread pool |
| Throughput | 50,000 TPS | Horizontal Rules Engine, 6 partitions |
| Availability | 99.9% | Circuit breakers, DLQ, no SPOF |
| Durability | Zero loss | Kafka 7d, PostgreSQL WAL, Redis AOF |
| Auth latency | < 5 ms | Stateless JWT, only Redis blacklist lookup |
3.6 Risks & Assumptions
| Risk | Impact | Mitigation |
|---|---|---|
| Kafka lag spike | HIGH | HPA scales Rules Engine on lag |
| Redis unavailable | MED | Velocity falls back to DB; blacklist fails open |
| Duplicate Kafka delivery | CRIT | Idempotency key in Redis, 24h TTL |
| PostgreSQL failure | CRIT | Patroni failover to standby |
Tech Stack & Why
Every technology was chosen for production use. These are the same tools running in real fraud detection systems at scale.
| Technology | Role | Why this, not X |
|---|---|---|
| Java 21 LTS | Language | Virtual threads, record patterns, sequenced collections, long support. |
| Spring Boot 4.1 | Framework | Spring Framework 7, Jakarta EE 11, auto-config, embedded Tomcat, actuators. |
| Spring Security 7 | Auth & RBAC | Industry standard. Never roll your own auth. |
| Apache Kafka | Event streaming | Retention + replay, consumer groups. RabbitMQ deletes on ACK. |
| Redis 7 | Windows + blacklist | Sub-ms ZADD, TTL, shared across instances. |
| PostgreSQL 16 | Store | ACID, JSON columns, JPA integration. |
| JWT (jjwt) | Stateless tokens | No DB lookup per request โ linear scaling. |
| Resilience4j | Circuit breaker | Pure Java, Spring native. Hystrix deprecated. |
| Docker Compose | Local orchestration | One command starts all 9 containers. |
Service Catalog
Transaction Service
REST entry point. Validates, persists, publishes to Kafka.
:8081Rules Engine
Stateful Kafka consumer. Scores against velocity, amount, geo.
:8082Alert Service
Consumes verdicts. Creates alerts, triggers notifications.
:8083Admin API
Control plane. Rule management, overrides, audit logs.
:8084Auth Service
JWT issuance, refresh rotation, Redis blacklist.
:8085Notification Worker
Sends email/SMS (mocked in dev). Idempotent + DLQ.
:8086Transaction Service
The sole public-facing entry point. Receives raw payment events, validates them, persists to PostgreSQL, and publishes to raw-transactions. The JWT filter rejects unauthenticated calls before the controller. It does not evaluate fraud โ its single responsibility is reliable, at-least-once delivery to the message bus.
- Port
- 8081
- Base Path
/api/transactions- Auth
- Bearer JWT (ROLE_USER+)
- Publishes
raw-transactions- Depends on
- Auth Svc, Kafka, PostgreSQL
- Scaling
- 3 replicas, CPU HPA
| Endpoint | Method | Response |
|---|---|---|
| /api/transactions | POST | 202 Accepted โ queued |
| /api/transactions/{id} | GET | 200 OK โ details + verdict |
| /api/transactions/my | GET | 200 OK โ paginated own list |
Rules Engine Service
The analytical core. Consumes raw-transactions, evaluates each against active rules from PostgreSQL, computes a composite score, persists the verdict, and publishes to fraud-verdicts. Rules refresh every 60 seconds โ no restart to change thresholds. The velocity check uses a Redis Sorted Set as a sliding-window counter.
- Port
- 8082 (internal)
- Consumes
raw-transactions- Publishes
fraud-verdicts- Idempotency
- Redis
processed:{txId}24h TTL - Scaling
- HPA on Kafka lag > 1000
Velocity Check โ Redis Sliding Window
ZADD velocity:{userId} {epochMs} {txId}
ZREMRANGEBYSCORE velocity:{userId} 0 {epochMs - 60000}
count = ZCARD velocity:{userId}
EXPIRE velocity:{userId} 60
if (count > 5) score += 40;
Why a sorted set, not INCR? A fixed window resets at boundaries โ an attacker could send 5 txns at 11:59:59 and 5 more at 12:00:01 and never trigger. Sorted sets give a true sliding window.
| Rule | Backend | Threshold | Weight |
|---|---|---|---|
| Velocity Check | Redis ZADD | > 5 / 60s | 40 |
| Amount Anomaly | PostgreSQL avg | > 3ร 30-day avg | 30 |
| Geo-Mismatch | PostgreSQL | Different country | 20 |
| Duplicate | Redis | Same amt+merchant / 10s | 10 |
| Flag threshold | Score โฅ 50 โ FLAGGED | โ | |
Alert Service
Bridges the fraud pipeline and the end user. Consumes fraud-verdicts, persists an alert for every FLAGGED verdict, and publishes to alert-notifications. Users can fetch only their own alerts โ the query is scoped to the JWT subject.
- Port
- 8083
- Consumes
fraud-verdicts- Publishes
alert-notifications- States
- PENDING โ SENT โ ACKNOWLEDGED
- Resilience
- Circuit breaker on notify publish
Admin API
The control plane for analysts. View flagged transactions with dynamic filtering, override verdicts, manage rules without redeployment, inspect the audit log. Every write is captured as an immutable audit event. Uses JPA Specifications for dynamic query filtering.
- Port
- 8084
- Auth
- ROLE_ANALYST / ROLE_ADMIN
- Owns
- fraud_rules, audit_logs
- Rate Limit
- 100 req / 10s per user
Security note: Every /api/admin/** endpoint requires ROLE_ANALYST or ROLE_ADMIN. A ROLE_USER gets 403 Forbidden โ not 401. 401 = "who are you?", 403 = "I know who you are, and the answer is no."
Auth Service
The identity backbone โ the only service that issues tokens. Manages users, roles, password hashing, JWT generation, refresh rotation, and logout via Redis blacklisting. Other services validate JWTs locally without calling back; only the blacklist check hits shared storage.
- Port
- 8085
- Access Token TTL
- 15 minutes
- Refresh Token TTL
- 7 days (hashed in DB)
- Password hashing
- BCrypt cost 12
- Token signing
- HMAC-SHA256, 256-bit key
- Roles
- USER, ANALYST, ADMIN, SUPER_ADMIN
Why 15 min for the access token? If a JWT is stolen, the attacker can use it until it expires. 15 minutes limits the blast radius. The client silently refreshes, so the user never notices.
Why store refresh tokens hashed? Same reason as passwords. If the DB is breached, the attacker cannot use the hashes to issue new JWT pairs. The raw token travels only over HTTPS.
Notification Worker
Consumes alert-notifications and dispatches email/SMS through configured providers (SendGrid for email, Twilio for SMS in production). Idempotent: a notification ID is stored after successful dispatch. After 3 failed retries, messages go to alert-notifications.DLT.
- Port
- 8086 (internal)
- Consumes
alert-notifications- Dead Letter
alert-notifications.DLT- Retry
- 3 attempts, backoff 1s/2s/4s
- Prod
- SendGrid (email), Twilio (SMS)
๐ง Backend โ Code Deep Dive
How to read this section. The six services above describe the target microservice architecture. The code that actually ships in backend/ is a single Spring Boot application (one pom.xml, one deployable JAR) that consolidates those capabilities into controllers and @Service beans. This section documents that real code โ every class, every public method, and what each one does.
- Shape
- Single Spring Boot app (modular monolith)
- Base package
com.fraudshield- Controllers
- 5 (
Auth,Transaction,Admin,Rule,Config) - Service beans
- 7 (
Auth,Jwt,CustomUserDetails,Transaction,RulesEngine,Rule,Audit) - Persistence
- Spring Data JPA, H2 (dev) / PostgreSQL (prod)
- Fraud scoring
- Deterministic weighted rules (no ML)
Layered Architecture
Requests flow through a classic Spring layering: the JwtAuthenticationFilter authenticates, a controller maps the HTTP endpoint, a service holds the business logic, and a repository talks to the database. DTOs cross the controller boundary; entities stay inside the service/repository layers.
HTTP โโถ JwtAuthenticationFilter โโถ Controller โโถ Service โโถ Repository โโถ DB
โ โ
(DTOs) (entities)
Controllers โ the HTTP surface
| Controller | Base path | Endpoint โ method | What it does |
|---|---|---|---|
| AuthController | /api/auth | POST /login โ login() | Validates credentials, returns access + refresh tokens. |
POST /refresh โ refresh() | Exchanges a valid refresh token for a fresh token pair. | ||
POST /logout โ logout() | Revokes the caller's active refresh tokens; returns 204. | ||
GET /me โ me() | Returns the current user (name, email, role) from the JWT subject. | ||
| TransactionController | /api/transactions | GET / โ list() | Lists transactions with optional q/verdict/status filters. |
GET /{id} โ get() | Fetches one transaction with its triggered rules. | ||
POST / โ create() | Creates a transaction, scores it, returns 202 Accepted. | ||
| AdminController | /api/admin | GET /transactions โ transactions() | Analyst view of all transactions (same filters as above). |
PUT /verdicts/{id} โ applyVerdict() | Overrides a verdict: FALSE_POSITIVE or CONFIRMED_FRAUD. | ||
GET /audit-logs โ auditLogs() | Returns the audit trail. ADMIN / SUPER_ADMIN | ||
| RuleController | /api/admin/rules | GET / โ list() | Lists fraud rules sorted by weight (descending). |
PUT /{id} โ update() | Enables/disables a rule or changes its weight. ADMIN / SUPER_ADMIN | ||
| ConfigController | /api/config | config endpoint(s) | Exposes runtime config such as mock-mode state to the UI. |
Service Beans โ the business logic
RulesEngineService the analytical core
Scores a transaction against the enabled fraud rules and decides a verdict. This is where "is this fraud?" is answered.
score(Transaction)โ loads enabled rules viafindByEnabledTrue(), sums the weight of every rule thatfires(), and returns aScoreResult. Score โฅ 50 โ FLAGGED (statusPENDING); otherwise PASSED.fires(FraudRule, Transaction)โ the per-rule evaluation logic:amountfires when amount > 2000;nightowlfires when amount > 1000 and the UTC hour is 2โ5;velocity/geo/duplicatefire on a deterministic hash bucket ofid + merchantId.bucket(Transaction, salt)โ produces a stable 0โ99 bucket so transaction scoring is deterministic and reproducible across restarts.ScoreResult(record) โ carriesscore,verdict,status, and the list of fired rule IDs.
Current state vs. production design: The shipped code uses H2 in-memory storage and hash-bucket scoring. The formal architecture (above) describes Redis for token revocation and sliding-window velocity checking. These are future enhancements โ Redis support is planned for the production deployment.
TransactionService
create(request, userId, ip)โ builds aTransaction(generates atx-id, derives country from lat/long), callsRulesEngineService.score(), persists it, writes an audit entry, returns aCreateTransactionResponse.list(q, verdict, status)โ builds a JPASpecificationfrom the filters, returns newest-first; filters data rows appropriately based on access level.get(id)โ fetches one transaction or throwsNotFoundException.applyVerdict(id, decision, actor, ip)โ analyst override:FALSE_POSITIVEโ statusOVERRIDDEN;CONFIRMED_FRAUDโ flagsconfirmedFraud; logs the action.primeSequence()โ initializes the transaction id counter, ensuring no collisions across restarts.- helpers:
buildSpec(),ruleIndex(),toResponse(),deriveCountry().
AuthService & JwtService authentication
- AuthService.
login()โ verifies the BCrypt password, audits, issues tokens.refresh()โ one-time-use rotation: marks the old refresh token used, issues a new pair.logout()โ marks all active refresh tokens used.me()โ returns the currentUserDto.issueTokens()stores a SHA-256 hash of the refresh token (never the raw value). - JwtService.
generateAccessToken()โ signs an HS256 JWT withsubject,role,nameclaims (15-min TTL).generateRawRefreshToken()โ 256-bit random, URL-safe.parseAndValidate()โ verifies signature + expiry.sha256Hex()โ hashes refresh tokens for storage. - CustomUserDetailsService.
loadUserByUsername(email)โ Spring Security hook; loads the user and maps their role to a granted authority.
RuleService
list()โ returns all rules sorted by weight (highest first).update(id, request, actor, ip)โ togglesenabledand/or changesweight, builds a human-readable audit message describing exactly what changed, and persists it.
AuditService cross-cutting
log(actor, action, ip, icon)โ appends an immutableAuditLogrow (called by nearly every write path).recent()โ returns the audit trail newest-first, filtered by access level.resolveClientIp(request)โ prefers the firstX-Forwarded-Forhop, else the remote address.
Request lifecycle โ a POST /api/transactions
- 1
JwtAuthenticationFiltervalidates the Bearer token and sets the security context. - 2
TransactionController.create()reads the JWT subject as the user id and delegates to the service. - 3
TransactionService.create()builds the entity and callsRulesEngineService.score(). - 4
RulesEngineServicesums enabled-rule weights โscoreโฅ 50 isFLAGGED. - 5The verdict is persisted,
AuditService.log()records it, and a202 Acceptedreturns.
๐งญ Eureka Service Discovery
The backend registers itself with a Netflix Eureka server at startup so that other services can find it by logical name instead of a hardcoded host and port. Eureka is the service registry โ the phone book of the system: every instance checks in, sends a periodic heartbeat, and is removed automatically if it stops responding.
Why discovery matters. In a microservice deployment, instances come and go and their IPs change. Rather than wiring one service to http://10.0.1.7:8082, a caller asks Eureka for FRAUDSHIELD-BACKEND and gets a live address. This is what lets the Rules Engine scale to 6ร replicas without anyone editing a config file.
- Role
- Eureka client (registers & discovers)
- Starter
spring-cloud-starter-netflix-eureka-client- Spring Cloud
2025.1.4(Northfields)- Registry server
http://localhost:8761/eureka/- Registers as
fraudshield-backend:8080- Toggle
EUREKA_ENABLED(defaulttrue)
Configuration (application.yml)
Discovery is on by default and can be disabled with a single environment variable โ handy for running the backend standalone in local dev or tests.
spring:
application:
name: fraudshield-backend # the name peers look up in Eureka
cloud:
discovery:
enabled: ${EUREKA_ENABLED:true}
eureka:
client:
enabled: ${EUREKA_ENABLED:true}
register-with-eureka: true # publish this instance to the registry
fetch-registry: true # pull the list of other services
service-url:
defaultZone: ${EUREKA_URL:http://localhost:8761/eureka/}
instance:
prefer-ip-address: true # register by IP, not hostname
instance-id: ${spring.application.name}:${server.port}
| Property | Value | What it does |
|---|---|---|
eureka.client.enabled | ${EUREKA_ENABLED:true} | Master switch for the Eureka client. Set EUREKA_ENABLED=false to run with no registry. |
register-with-eureka | true | Publishes this backend instance to the registry so others can find it. |
fetch-registry | true | Downloads and caches the registry so this app can call peers by name. |
service-url.defaultZone | ${EUREKA_URL:โฆ:8761/eureka/} | Where the Eureka server lives. Override with EUREKA_URL in other environments. |
instance.prefer-ip-address | true | Registers by IP instead of hostname โ avoids DNS issues in containers. |
instance.instance-id | name:port | Unique id for this instance, e.g. fraudshield-backend:8080. |
Environment overrides
| Variable | Default | Effect |
|---|---|---|
EUREKA_ENABLED | true | false โ backend runs standalone, no registration or lookup. |
EUREKA_URL | http://localhost:8761/eureka/ | Points the client at a different Eureka server (e.g. a prod cluster). |
How registration works
- 1Start Eureka first. The registry server must be up on
:8761before the backend boots (or the client will retry until it is). - 2Register. On startup the backend POSTs its instance info (
fraudshield-backend, IP, port8080) to the Eureka server. - 3Heartbeat. The client renews its lease every ~30s. Miss enough heartbeats and Eureka evicts the instance.
- 4Discover. With
fetch-registry: true, the backend caches the registry and can resolve other services by their logical name. - 5Shut down. On graceful stop the client de-registers so no caller is routed to a dead instance.
Dev tip. Running just the backend without a registry? Set EUREKA_ENABLED=false โ otherwise you'll see connection-refused retries in the logs while it tries to reach :8761. The app still works; the warnings are only about discovery.
๐ฎ Postman โ Modify the Collection
The repo ships an importable collection at Postman/FraudShield.postman_collection.json. It is a v2.1.0 collection that already wires up authentication, variables, and token auto-capture โ so extending it is mostly a matter of knowing where each piece lives. This section is a guide to changing and growing the collection, not just running it.
Anatomy. A Postman collection is JSON with four parts that matter here: variable (collection variables), auth (collection-wide auth), item (folders and requests, nestable), and per-request event scripts (pre-request / test). Edit any of them in the Postman UI, or directly in the JSON file and re-import.
How this collection is wired
| Piece | Where | What it does |
|---|---|---|
| Collection variables | variable[] | baseUrl, accessToken, refreshToken, lastTxId โ shared across every request. |
| Collection auth | auth (top level) | Bearer token bound to {{accessToken}}; inherited by every request unless overridden. |
| Folders | item[] with nested item[] | Auth, Transactions, Admin, Fraud Rules, Config. |
| Token capture | Login request event โ test script | Saves accessToken/refreshToken via pm.collectionVariables.set(...). |
| Chaining | Create-tx test script | Saves transactionId into lastTxId so later requests reuse it. |
1. Add a new request to a folder
Append a request object inside the target folder's item[] array. The minimum shape:
{
"name": "Create Transaction โ custom",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"amount\": 999.00,\n \"currency\": \"EUR\",\n \"merchantId\": \"MERCH-TEST-01\"\n}"
},
"url": {
"raw": "{{baseUrl}}/api/transactions",
"host": ["{{baseUrl}}"],
"path": ["api", "transactions"]
}
}
}
Always build the URL with {{baseUrl}} and a split path array โ never hardcode http://localhost:8080. That keeps the collection portable across environments.
2. Change the body or query params
The request body lives in request.body.raw as an escaped JSON string (note the \n and \"). Edit it in Postman's body editor to avoid escaping by hand. For query params, add a query array and mirror it in url.raw:
"url": {
"raw": "{{baseUrl}}/api/transactions?verdict=FLAGGED&status=PENDING",
"host": ["{{baseUrl}}"],
"path": ["api", "transactions"],
"query": [
{ "key": "verdict", "value": "FLAGGED" },
{ "key": "status", "value": "PENDING" }
]
}
3. Add or reuse a variable
Add a new entry to the top-level variable[] array, then reference it anywhere as {{yourVar}}:
{ "key": "merchantId", "value": "MERCH-TEST-01", "type": "string" }
Read it in a script with pm.collectionVariables.get('merchantId') and write it with pm.collectionVariables.set('merchantId', value). Prefer collection variables for values shared by many requests; use an environment when the same collection must run against dev / staging / prod.
4. Capture a value from a response (chaining)
Add a test script to the request's event[]. This is exactly how the login request feeds the token into every later call:
"event": [{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.collectionVariables.set('accessToken', json.accessToken);",
"pm.collectionVariables.set('refreshToken', json.refreshToken);"
]
}
}]
Use the same pattern to grab a new id โ e.g. pm.collectionVariables.set('lastTxId', json.transactionId) โ then reference {{lastTxId}} in the next request's URL.
5. Override auth for one request
Every request inherits the collection's Bearer auth. For a public endpoint (login, refresh, mock-status) opt out per request so no stale token is sent:
"request": {
"auth": { "type": "noauth" },
"method": "POST",
"...": "..."
}
6. Switch user / role
Role-gated endpoints (rule writes need ADMIN) depend on which login you ran last, because each login overwrites accessToken. To test a 403, run Login (HK Analyst), then call a rule PUT. To duplicate a login for another account, copy the login request and change only the email/password in the body.
7. Add the mock-mode header
To force seeded data on a list request, add the optional override header; omit it to use the server config:
"header": [
{ "key": "X-Mock-Response", "value": "true" }
]
Re-import & verify
- 1Edit the JSON (or edit in Postman and Export โ overwrite the file to keep the repo in sync).
- 2In Postman, Import the file again โ choose Replace so you don't end up with duplicates.
- 3Run Auth โ 1. Login (HK Admin) first to refresh
{{accessToken}}, then your new request. - 4Watch the created data appear live in the UI at
http://localhost:5173.
Keep the JSON valid. A trailing comma or an unescaped quote in a raw body will make Postman reject the import. When in doubt, make the change in the Postman UI and export rather than hand-editing the escaped body string.
End-to-End Request Flow
Traces a single flagged transaction from the client's HTTP call to the user's email alert. This is what an interviewer expects when they ask "walk me through your system."
- 1Client Authentication: POST with Bearer JWT. Filter validates signature, expiry, Redis blacklist. 401 on failure.
- 2Payload Validation: Bean Validation checks fields. 400 with field errors on failure.
- 3Persist + Publish: Saved to PostgreSQL, published to Kafka. 202 returned after Kafka ACK.
- 4Rules Evaluation: Redis sliding window finds 6 txns/60s. Score = 70 โ FLAGGED.
- 5Alert Creation: Alert Service creates alert, publishes to alert-notifications.
- 6User Notification: Worker checks idempotency key, sends mock email.
Configuration Model
Follows 12-Factor App principles: every environment-specific value is externalized. No secrets in code โ only Spring profiles and environment variables.
| Variable | Default (dev) | Notes |
|---|---|---|
JWT_SECRET | dev-secret-256bit | โฅ 256 bits. Prod: Kubernetes Secret. |
JWT_EXPIRY_MS | 900000 | Access token TTL (15 min) |
REFRESH_TOKEN_EXPIRY_DAYS | 7 | Refresh token TTL |
KAFKA_BOOTSTRAP_SERVERS | localhost:9092 | Compose: kafka:9092 |
REDIS_HOST | localhost | Compose: redis |
FRAUD_SCORE_THRESHOLD | 50 | Min score to flag |
VELOCITY_MAX_COUNT | 5 | Max txns per window |
Never hardcode secrets. In production, JWT_SECRET must come from a secrets manager. A hardcoded secret is a critical OWASP vulnerability.
Resilience & Fault Tolerance
FraudShield treats failures as normal. Every integration point is wrapped with a Resilience4j pattern. The goal is graceful degradation.
| Pattern | Applied On | Config | Fallback |
|---|---|---|---|
| Circuit Breaker | Alert โ Notify | 50% / 10 calls; 30s | Alert stays PENDING |
| Retry | Rules โ Kafka | 3 attempts, 1/2/4s | Message to DLT |
| Rate Limiter | Admin API | 100 req / 10s | 429 + Retry-After |
| Bulkhead | Rules eval | Max 50 concurrent | 503 |
| DLQ | All consumers | After 3 retries | Manual review, no loss |
| Idempotency | Rules, Notify | Redis key, 24-48h | Duplicates skipped |
Security
Five layers. Bypassing one alone is not enough โ defence in depth.
| Layer | Mechanism | OWASP |
|---|---|---|
| L1 Transport | HTTPS / TLS 1.3 | A02 Crypto Failures |
| L2 Authentication | JWT Bearer, Redis blacklist | A07 Auth Failures |
| L3 Authorization | RBAC (URL + @PreAuthorize) | A01 Broken Access Control |
| L4 Validation | Bean Validation on DTOs | A03 Injection |
| L5 Data | BCrypt, hashed tokens, masked logs | A02, A09 |
Handling an Unauthenticated Request
- 1Request arrives. JwtAuthenticationFilter runs before the controller.
- 2Filter checks for Bearer token. Absent โ SecurityContext anonymous.
- 3Token validation: signature, expiry, Redis blacklist. Any failure โ anonymous.
- 4Spring Security evaluates the route. Public routes pass; protected require auth.
- 5401 Unauthorized returned as structured JSON โ no stack trace. Source IP logged.
401 vs 403: 401 = "not authenticated." 403 = "authenticated, no permission." FraudShield returns 401 for invalid tokens, 403 for insufficient roles.
Auth & Token Flow (Three Layers)
Token Refresh Flow
POST /api/auth/refresh { "refreshToken": "UUID" }
Server:
1. Hash incoming token (SHA-256)
2. Look up hash in PostgreSQL
3. Check: not expired, not already used
4. DELETE old refresh token (rotation โ single use)
5. Issue new JWT (15 min) + new refresh token (7 days)
6. Invalid โ 401, client must re-login
Why single-use refresh tokens? If stolen and used by an attacker, the legitimate client's next refresh fails (already consumed) โ alerting the system to theft. Both parties are forced to re-login.
Observability
The three pillars โ logs, metrics, traces โ are all implemented. In dev everything goes to the console as structured JSON.
| Pillar | Technology | Key Data |
|---|---|---|
| Logging | SLF4J + Logback (JSON) | Correlation ID, userId, traceId. Sensitive data masked. |
| Metrics | Micrometer + Actuator | transactions.total, fraud.rate, rules.eval.time, Kafka lag |
| Tracing | Sleuth + Zipkin | traceId propagated via HTTP + Kafka headers across 6 services |
| Health | /actuator/health | DB, Kafka, Redis connectivity |
Correlation ID: Every request gets a UUID added to the MDC, so every log line carries it โ across all services including Kafka processing. Tracing one transaction through 6 services becomes trivial.
Failure Scenarios
| Scenario | Impact | Behaviour | Recovery |
|---|---|---|---|
| Kafka down | DEGRADED | Transaction Svc buffers, 503 after full. Rules Engine pauses. | Auto on restart. No loss. |
| Redis down | DEGRADED | Velocity falls back to DB. Blacklist fails open (logged). | Auto reconnect. |
| Rules Engine down | DEGRADED | Transactions accumulate in Kafka (7d). | Processes backlog in order. |
| PostgreSQL down | CRITICAL | DB-dependent services return 503. | Patroni failover. |
| Duplicate delivery | HANDLED | Idempotency key check. Skipped. | Inline. |
| Stolen JWT | RISK | Usable up to 15 min. Logout blacklists it. | Short TTL limits window. |
| Invalid JWT | HANDLED | 401 immediately. IP logged. Rate limited. | Inline. |
UI Overview โ The Analyst Dashboard
In one line: The FraudShield frontend is a React + Vite dashboard where fraud analysts view, review, and override flagged payments โ exactly like the internal tool a real bank's fraud team would use.
So far this guide has covered the backend โ the services, Kafka pipeline, and security that decide whether a payment is fraud. This part covers the frontend: the screen a human fraud analyst actually looks at. The backend makes the decision; the frontend lets a person review and act on it.
The dashboard lives in the UI/ folder and runs completely on mock data โ no backend connection needed. It is the visual counterpart to the Admin API service documented earlier.
Dashboard
KPI cards and a live list of recently flagged transactions.
Transactions
Searchable, filterable table with a detail drawer and verdict override.
Fraud Rules
Enable/disable detection rules (admins only โ RBAC enforced in the UI).
Audit Log
Immutable timeline of every analyst action.
Frontend Stack & Why
| Technology | Role | Why this, not X |
|---|---|---|
| React 18 | UI library | Component-based, huge ecosystem, what most MNCs expect. Re-renders only what changes. |
| Vite | Build tool / dev server | Instant hot-reload, near-zero config. Much faster than Create-React-App (now deprecated). |
| React Router 6 | Client-side routing | Maps URLs (/transactions, /rules) to pages without full reloads; guards protected routes. |
| Context API | Auth state | Lightweight built-in state sharing for the logged-in user โ no need for Redux at this size. |
| CSS variables | Theming | Same design tokens as this guide. One variable swap flips the whole app to dark mode. |
| localStorage | Session + theme | Keeps you logged in and remembers your theme across refreshes โ mirrors how a JWT would be held client-side. |
The frontend is intentionally decoupled from the backend. Today it reads from mockData.js; swapping that one file for real fetch() calls to the Admin API would connect it to the live system without touching any component.
Folder Structure
UI/
index.html Vite entry, loads fonts + favicon
vite.config.js dev server on port 5173
package.json react, react-dom, react-router-dom
src/
main.jsx React root + Router
App.jsx routes + auth guard
auth.jsx mock login, session in localStorage
mockData.js users, transactions, rules, audit log
styles.css design system (brand tokens, dark mode)
components/
Layout.jsx sidebar + topbar shell
pages/
Login.jsx sign-in screen
Dashboard.jsx KPI cards + recent flags
Transactions.jsx table + detail drawer + override
Rules.jsx rule toggles (RBAC)
Audit.jsx action timeline
Login Screen
The entry point. It validates credentials against the mock user list, stores a session in localStorage, and redirects to the dashboard. Clicking a demo account auto-fills the form โ handy for a quick demo.
How It Works (example)
When you type HKanalyst@fraudshield.io / Analyst@123 and hit Sign In, the login() function in auth.jsx searches the mock user list. On a match it saves {name, email, role} to React state and localStorage, then React Router redirects to /. On a mismatch it shows a red "Invalid email or password" banner. In the real system this exact step would instead call POST /api/auth/login and store the returned JWT.
Dashboard
The landing page after login. Four KPI stat cards give an at-a-glance health check, followed by a table of the most recent flagged transactions.
Example: the "Flagged" card
The flagged count is computed live from the data: TRANSACTIONS.filter(t => t.verdict === 'FLAGGED'). The card also shows the fraud rate as a percentage. Because it is derived in React, if a new flagged transaction arrived (in the real system, via a Kafka-driven update), the number would re-render automatically.
Transactions + Detail Drawer
The core working screen. A searchable, filterable table of all transactions. Clicking any row slides open a detail drawer from the right showing the full transaction, which fraud rules fired and their scores, and โ for analysts โ buttons to override the verdict.
Example: overriding a verdict
Suppose analyst Priya opens tx-10024. The drawer shows it scored 70 because the Velocity Check (+40) and Amount Anomaly (+30) rules both fired. Priya recognises it as a legitimate large purchase and clicks โ Mark False Positive. The row's status instantly updates to OVERRIDDEN, and in the real system this would fire PUT /api/admin/verdicts/tx-10024 and write an entry to the audit log. The override buttons only appear for analysts and admins โ a plain user never sees them (RBAC).
Fraud Rules
Lists every detection rule with its score weight and an on/off toggle. This screen demonstrates role-based access control in the UI: only admins can flip the toggles. Analysts see the rules but the switches are locked, with a notice explaining the restriction โ the frontend mirror of a backend 403.
Example: the RBAC difference
Log in as HKadmin@fraudshield.io and the toggles are live โ click one and the rule flips on/off instantly. Log in as HKanalyst@fraudshield.io and the same toggles are greyed out with a banner: "Only Admins can enable or disable rules." This is the same authorization boundary the backend enforces, surfaced in the UI so analysts aren't shown controls they can't use.
Audit Log
A chronological timeline of every action analysts and admins have taken โ verdict overrides, rule changes, logins, report exports. Each entry records who did it, from which IP, and when. In a real system this is critical for compliance and for investigating disputes.
Component Reference
Every reusable piece of the UI, what it does, and where it lives. This is the "parts list" an engineer would read before changing the frontend.
| Component | File | What it does |
|---|---|---|
| AuthProvider | auth.jsx | React Context that holds the logged-in user. Exposes login(), logout(), and user to the whole app. Persists the session in localStorage. |
| Protected | App.jsx | Route guard. If no user is logged in, redirects to /login. Wraps every dashboard route. |
| Layout | components/Layout.jsx | The app shell: fixed sidebar (nav links + sign-out) and top bar (page title, theme toggle, user avatar). Renders the active page via <Outlet />. |
| Login | pages/Login.jsx | Sign-in form with validation, error banner, and clickable demo-account hints. |
| Dashboard | pages/Dashboard.jsx | Four KPI stat cards + a recent-flagged table. All figures derived live from the data. |
| Transactions | pages/Transactions.jsx | Search + filter table, the detail drawer, and the override logic. The most complex page. |
| Rules | pages/Rules.jsx | Rule cards with toggles. Enforces admin-only editing (RBAC). |
| Audit | pages/Audit.jsx | Read-only action timeline. |
Shared UI Patterns (CSS classes)
| Class | Looks like | Used for |
|---|---|---|
.stat | White card, big number, small label | Dashboard KPI cards |
.chip | Small coloured pill | Status/verdict tags: FLAGGED PASSED PENDING |
.table-card | Rounded table with sticky header | Transactions & dashboard lists |
.drawer | Panel sliding in from the right | Transaction detail view |
.toggle | iOS-style on/off switch | Enabling/disabling fraud rules |
.score-bar | Mini progress bar + number | Showing fraud score (red โฅ 50) |
.role-badge | Tiny blue pill | Showing the user's role in the top bar |
Run it yourself: cd UI then npm install and npm run dev. Open http://localhost:5173 and sign in with any demo account. Everything works on mock data โ no backend required.
Build & Deployment
Prerequisites
- Java 21+ (
java -version) - Maven 3.9+ (
mvn -version) - Docker Desktop (for full-stack mode)
Option 1 โ Mock Mode (Zero Infrastructure)
cd auth-service
mvn spring-boot:run -Dspring-boot.run.profiles=default
# Swagger UI: http://localhost:8085/swagger-ui.html
# H2 Console: http://localhost:8085/h2-console (jdbc:h2:mem:fraudshielddb, sa, blank)
Option 2 โ Full Stack (Docker Compose)
docker-compose up --build
# All 9 containers: Kafka, Zookeeper, Redis, PostgreSQL + 6 services
# Auth: http://localhost:8085/swagger-ui.html
Pre-seeded Accounts (Mock Data)
| Password | Role | |
|---|---|---|
HKsuperadmin@fraudshield.io | SuperAdmin@123 | ROLE_SUPER_ADMIN |
HKadmin@fraudshield.io | Admin@123 | ROLE_ADMIN |
HKanalyst@fraudshield.io | Analyst@123 | ROLE_ANALYST |
HKuser@fraudshield.io | User@123 | ROLE_USER |
Quick API Test
# 1. Login
curl -X POST http://localhost:8085/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"HKuser@fraudshield.io","password":"User@123"}'
# 2. Submit a transaction (use token from step 1)
curl -X POST http://localhost:8081/api/transactions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount":9999.99,"currency":"USD","merchantId":"MERCH-001"}'
# 3. Check alerts
curl http://localhost:8083/api/alerts -H "Authorization: Bearer $TOKEN"
๐ Setup Guide โ Clone & Run It Yourself
If you've actually read this far โ the HLD, the six-service story, the Eureka deep-dive, all of it โ respect. Time to stop reading about FraudShield and actually run it. Clone the real repo and see it boot in under 5 minutes.
Honest disclosure: everything above (6 microservices, Kafka, Docker Compose with 9 containers) describes the target architecture this guide is built around. What's actually in the GitHub repo is a single Spring Boot modular monolith โ same ideas (JWT, RBAC, rules engine, Eureka, audit trail), way less infra to spin up. The steps below match the real, shipped code. ๐ฆ View on GitHub
What You Need
| Part | Runs on | Needs |
|---|---|---|
| Backend | :8080 | Java 17, Maven 3.9+ |
| UI | :5173 | Node 18+, npm |
| Eureka server (optional) | :8761 | only if EUREKA_ENABLED=true |
Step 1 โ Clone It
git clone https://github.com/hanyak10/FraudShield.git
cd FraudShield
Real layout you'll see on disk:
fraudshield/
โโโ backend/ Spring Boot 3.2 monolith (Java 17, H2, JWT, Eureka client)
โโโ UI/ React 18 + Vite analyst dashboard
โโโ Postman/ Importable Postman collection (auto-manages tokens)
โโโ api-request-bodies.json Postman-ready request-body reference
โโโ README.md You are here
Step 2 โ Run the Backend
cd backend
mvn spring-boot:run
# Fresh, seeded H2 database every restart โ no setup needed.
# Swagger UI : http://localhost:8080/swagger-ui.html
# H2 console : http://localhost:8080/h2-console (jdbc:h2:mem:fraudshield, user sa, blank password)
# Health : http://localhost:8080/actuator/health
Running standalone without a Eureka registry handy? Skip discovery so it doesn't retry connections forever:
# PowerShell
$env:EUREKA_ENABLED="false"; mvn spring-boot:run
# bash
EUREKA_ENABLED=false mvn spring-boot:run
Step 3 โ Run the UI
cd UI
npm install
npm run dev
# Opens http://localhost:5173 โ backend already allows CORS for this origin.
Step 4 โ Eureka Server (Optional)
The backend is a Eureka client; it expects a registry at http://localhost:8761/eureka/. Start any standard Eureka server on :8761 first if you want to see live service-discovery registration (the same check this guide's v1.1.0 upgrade verified) โ or just set EUREKA_ENABLED=false and skip it entirely.
Step 5 โ Log In
| Password | Role | Can do | |
|---|---|---|---|
HKadmin@fraudshield.io | Admin@123 | ROLE_ADMIN | Everything, incl. toggling rules |
HKanalyst@fraudshield.io | Analyst@123 | ROLE_ANALYST | Review + override verdicts |
HKsuperadmin@fraudshield.io | SuperAdmin@123 | ROLE_SUPER_ADMIN | Everything |
Prefer Postman?
Import Postman/FraudShield.postman_collection.json and start the backend. Running Auth โ 1. Login (HK Admin) auto-saves accessToken/refreshToken as collection variables, and every other request attaches Authorization: Bearer {{accessToken}} for you โ no copy-pasting tokens by hand.
Mock vs. Real Data
The backend serves either seeded demo rows (mock) or only what you create at runtime (real). Force seeded data on any list call with a header:
curl -H "X-Mock-Response: true" http://localhost:8080/api/transactions
That's it. No Kafka cluster, no Postgres container, no nine-service orchestra to wrangle โ just mvn spring-boot:run, npm run dev, and a login screen. Go flag some fake fraud.
Glossary
| Term | Definition |
|---|---|
| JWT | Stateless, signed token carrying claims. No DB lookup to validate โ just verify the signature. |
| Refresh Token | Long-lived single-use token (hashed in DB) to obtain new JWT pairs without re-login. |
| Token Rotation | Each refresh invalidates the old token and issues a new pair. |
| Token Blacklist | Redis set of revoked JWT IDs (jti) with TTL. Honours logouts before natural expiry. |
| Kafka Topic | Named, ordered, append-only log of events. Producers write; consumer groups read. |
| Consumer Group | Consumers sharing a group ID. Kafka distributes partitions โ one per consumer max. |
| Dead Letter Topic | Topic for messages that failed processing after all retries. |
| Idempotency | An operation producing the same result no matter how many times applied. |
| Sliding Window | Time-based counter over a rolling period. More accurate than a fixed window. |
| RBAC | Role-Based Access Control. Permissions on roles; roles on users. |
| Circuit Breaker | Stops calling a failing service, lets it recover, resumes once confirmed. |
| CQRS | Command Query Responsibility Segregation. Separates write from read models. |
| BCrypt | Password hashing with a cost factor. Cost 12 โ 250ms per hash. |
| HPA | Kubernetes Horizontal Pod Autoscaler. Scales on CPU or custom metrics. |
| jti | JWT ID claim. Unique per token. Used as the Redis blacklist key. |
| Versioning (X.Y.Z) | X = major app feature release, Y = tech stack/framework upgrade, Z = patch or bug fix. E.g. v1.1.0 is a Y-level tech stack upgrade (Java 21 + Spring Boot 4.1). |
FAQ
Why JWT and not server-side sessions?
Sessions require a shared store and a DB lookup per request. JWT is stateless โ the server only needs the secret key. You can scale to 100 instances with no shared auth state. The only shared state we add back is the Redis blacklist, a conscious tradeoff for logout support.
Why Kafka over RabbitMQ or REST calls?
Retention and replay. Kafka keeps messages 7 days regardless of consumption. If Rules Engine is down for an hour, every transaction is waiting when it recovers โ in order. RabbitMQ deletes on ACK. REST creates synchronous coupling.
How does the Rules Engine scale without double-processing?
Kafka consumer groups assign one partition to exactly one consumer, so instances partition the work without overlap. For rare redeliveries, the Redis idempotency key ensures double-processing is silently skipped.
What happens when an unauthenticated user hits a protected endpoint?
The JWT filter runs before any controller. With no valid token the SecurityContext stays anonymous and Spring Security returns 401 with structured JSON โ no stack trace. The request is logged with source IP.
Why BCrypt cost factor 12?
Cost is exponential. Cost 10 โ 75ms (too fast, GPU-brute-forceable). Cost 14 โ 1000ms (too slow under load). Cost 12 โ 250ms โ the sweet spot.
Why hash refresh tokens before storing?
If the DB is breached, plaintext refresh tokens let an attacker issue JWT pairs for every user. Hashing (SHA-256) gives the attacker only useless hashes. The raw token travels only over HTTPS.
How do you change fraud rules without redeploying?
Rules live in the fraud_rules table, not code. The Rules Engine re-queries every 60 seconds via a scheduled job. An analyst updates a threshold via the Admin API โ it takes effect within 60 seconds, no restart.
Can I replace H2 with PostgreSQL in dev?
Yes. Set the profile to dev, run docker-compose up postgres, update the DB env vars. JPA DDL auto-create handles schema generation in both โ the same entity classes work unchanged.
Version Control & Upgrade History
Release Timeline
| Version | Release Date | Changes | Status |
|---|---|---|---|
| v1.0.0 | Initial Release | Java 17 LTS, Spring Boot 3.2, deterministic rules engine, JWT auth, Eureka service discovery, Redis caching | โ ๏ธ EOL |
| v1.1.0 | Q4 2026 | Java 21 LTS, Spring Boot 4.1, Spring Security 7, Spring Cloud 2025.1.4, comprehensive test suite, live Eureka registration verified, performance optimizations (virtual threads) | โ Current |
Why Upgrade to v1.1.0? (Security Patches & Rationale)
FraudShield moved from v1.0.0 to v1.1.0 purely to adopt the latest LTS (Long-Term Support) versions of its core stack. This is a Y-level (tech stack) upgrade โ no new fraud-detection features were added, but every dependency now sits on a newer, better-patched foundation.
Security Patches & Upgrades Applied
- Java 17 โ Java 21 LTS: Picks up 2+ years of JDK security patches and enables virtual threads for lighter-weight concurrency.
- Spring Boot 3.2 โ 4.1 (Spring Framework 7): Brings the latest CVE fixes bundled in Spring's 7.x security releases and moves fully to the Jakarta EE 11 baseline.
- Spring Security 6 โ 7: Ships hardened defaults for the security filter chain and drops legacy, exploit-prone configuration patterns (like
.and()chaining). - Spring Cloud 2023.0.1 โ 2025.1.4: Updates the Eureka client library to a release with patched service-discovery dependencies.
- Jackson 2 โ Jackson 3: Closes known deserialization-related vulnerabilities present in older Jackson 2.x minor releases.
Why it matters: Running on an End-of-Life (EOL) Java 17 / Spring Boot 3.2 stack means no more security patches from upstream. Staying current keeps FraudShield protected against newly disclosed CVEs without waiting for a major feature release to justify the work.
v1.0.0 โ v1.1.0 Migration Guide
Breaking Changes
- Java 21 minimum requirement (was Java 17). Older JDKs are not supported.
- Spring Framework 7 (from 6): Jakarta EE 11 baseline โ no more
javax.*imports, all packages usejakarta.*. - Spring Security 7 (from 6): Lambda DSL required for security configuration; the
.and()method chaining pattern is removed. - Spring Boot starter rename:
spring-boot-starter-webis nowspring-boot-starter-webmvc. - Jackson 3 default (from 2): Package namespace changed to
tools.jackson.*. - Spring Cloud 2025.1.4 (from 2023.0.1): Ensure Eureka client version compatibility; the new release uses Spring Boot 4.1 as baseline.
Upgrade Checklist
- 1Update pom.xml dependencies: Change parent version from
3.2.5to4.1.2,java.versionfrom17to21,spring-cloud.versionfrom2023.0.1to2025.1.4,springdoc.versionfrom2.3.0to3.1.1. Renamespring-boot-starter-webtospring-boot-starter-webmvc. - 2Compile and check errors: Run
mvn clean compile. Compilation errors are usually minimal if code avoids deprecated APIs. Watch for import errors (javax โ jakarta). - 3Identify renamed config properties: Add
spring-boot-properties-migratortemporarily to pom.xml, runmvn spring-boot:run, check logs for property warnings, then remove the dependency. - 4Run full test suite: Execute
mvn test. If tests exist, they validate the upgrade. If none exist, add smoke tests (e.g., context-load tests to verify all 50+ beans initialize). - 5Boot and verify Eureka registration: Start both Eureka server and backend service. Check
http://localhost:8761/eureka/appsto confirm the client instance shows statusUPwith the correct port and hostname. - 6Verify all APIs: Test login (JWT token generation), token refresh, fraud rules engine scoring on seeded transactions, Swagger UI, and audit log endpoints.
What Was Tested in v1.1.0
- โ Full application context loads โ all 50+ Spring beans, security filter chain, JPA repository auto-configuration, JWT filter, Eureka auto-config
- โ Unit tests for rules engine โ 5 tests covering scoring logic, rule firing, velocity detection, amount anomaly, and edge cases
- โ
Live Eureka registration โ backend registers as
FRAUDSHIELD-BACKEND:8080with statusUPand remains discoverable - โ JWT authentication flows โ login endpoint, token generation with correct expiry (15 min access, 7d refresh), token refresh with rotation, logout with blacklist
- โ Fraud scoring on seeded data โ rules engine correctly identifies violations (velocity, amount anomaly, duplicates)
- โ Database compatibility โ H2 in-memory database (no migration required, schema auto-created via JPA DDL)
- โ Spring Security RBAC โ role enforcement on protected endpoints, 401/403 distinction
Zero application code changes required. The upgrade is a pure dependency bump. FraudShield's application layer already avoids deprecated APIs and follows modern Spring patterns. This demonstrates mature, production-ready code that survives major framework upgrades without refactoring.