A real-time fintech transaction processing platform built with Spring Boot microservices, Apache Kafka, PostgreSQL, MongoDB, Redis, RabbitMQ, Docker, and Spring Cloud.
The system processes financial transactions through an event-driven pipeline that includes validation, fraud detection, balance updates, notifications, and transaction archiving.
Copy .env.example to .env and replace every password, JWT_ACCESS_SECRET, and JWT_REFRESH_SECRET before starting the stack. Access and refresh secrets must be different. .env is intentionally ignored by Git.
The Docker Compose defaults expose only the frontend, API gateway, and local-only operational interfaces. Internal microservices, databases, and brokers are reachable only on the Docker network.
For an existing PostgreSQL volume, apply schema updates with ./manage.sh migrate before starting a version that requires a new table or column.
flowchart TB
FE[React Frontend :3000] --> GW[API Gateway :8080]
subgraph Docker[Docker Compose / fintech-network]
GW --> US[User Service :8081]
GW --> TS[Transaction Service :8083]
GW --> PR[Payment Rail Service :8089]
GW --> AS[Account Service :8082]
GW --> RS[Reporting Service :8086]
TS -->|internal account snapshot / ownership API| AS
AS -->|internal user snapshot API| US
TS -->|transaction-raw| K[(Kafka)]
K --> FS[Fraud Detection :8084]
FS -->|transaction-validated| K
K --> AS
AS -->|INTERNAL / HAVALE\natomic debit + credit| LEDGER[Double-entry Ledger]
AS -->|EFT / FAST\nreserve available balance| K
K -->|funds-reserved| PR
PR -->|idempotent rail attempt\ntransactional outbox| K
K -->|transfer-rail-result| AS
AS -->|settle or release reservation| LEDGER
AS -->|transaction-checked| K
K --> NS[Notification Service :8085]
NS --> RMQ[(RabbitMQ)]
NS -->|transaction-completed| K
K --> KC[Kafka Connect Sink]
GW -. service discovery .-> EU[Eureka :8761]
GW -. rate limit / session .-> REDIS[(Redis)]
end
TS --> PG[(PostgreSQL\nusers / accounts / transactions / ledger / outbox)]
AS --> PG
PR --> PG
FS --> PG
KC --> MONGO[(MongoDB\ncompleted transaction archive)]
RS --> MONGO
The pipeline resolves another platform user's IBAN as an atomic HAVALE. Transfers to an external bank reserve funds first; Payment Rail Service then simulates EFT/FAST execution idempotently, after which Account Service either settles the reservation into the ledger or releases it without losing money.
The local Docker environment uses one PostgreSQL instance with separate logical schemas, but domain services do not query or join another service's business tables at runtime:
user-serviceowns user and refresh-token data inuser_service.account-serviceowns accounts, reservations, ledger journals, inbox records, and account outbox events inaccount_service.transaction-serviceowns transaction state, status history, and transaction outbox events intransaction_service.- Transaction Service resolves account ownership, status, currency, and IBAN routing through Account Service's internal REST API.
- Account Service resolves beneficiary display names through User Service's internal REST API instead of joining
user_service.users. - Internal endpoints are not routed by the API Gateway and the service ports are not published to the host by Docker Compose.
- If an owning service is unavailable, callers fail explicitly with
503 SERVICE_UNAVAILABLErather than falling back to another service's tables.
This removes the runtime cross-schema coupling from the User–Account–Transaction domain flow while preserving the current single-instance local deployment. The shared audit component is a documented transitional exception: Account and Transaction services currently persist centralized audit records in audit_service. Extracting that storage behind an Audit Service or an event consumer is listed under Future Improvements.
- Real-time transaction processing
- Event-driven microservice architecture
- Fraud and AML validation pipeline
- Account balance update and transaction persistence
- Email / SMS / push notification support
- PostgreSQL for operational data
- MongoDB for completed transaction archive
- Redis for cache, session, and distributed lock support
- RabbitMQ for asynchronous notification delivery
- Explicit service-owned data boundaries with internal REST lookups instead of cross-schema domain queries
- Eureka-based service discovery
- API Gateway routing with Spring Cloud Gateway
- Immutable audit trail for account and transaction access/changes
- Rotating refresh tokens with reuse detection and server-side revocation
- Mandatory request idempotency and type-aware money-movement validation
- Enforced per-account daily spending limits and locked transaction state transitions
- Immutable double-entry ledger with account reconciliation APIs
- IBAN-based HAVALE/EFT/FAST routing with beneficiary verification
- ISO 13616 MOD-97 validation and checksum-correct Turkish IBAN generation
- Reserve-before-send external transfers with automatic release on rejection
- Idempotent Payment Rail attempts with hashed and masked IBAN persistence
- Idempotent Redis daily fraud aggregates using integer minor units
- Docker Compose environment for all services
- Kafka UI for topic monitoring
- Java 17
- Spring Boot
- Spring Cloud Gateway
- Spring Security
- Spring Data JPA
- Hibernate
- Spring Cloud Netflix Eureka
- Apache Kafka
- Kafka Connect
- RabbitMQ
- PostgreSQL
- MongoDB
- Redis
- React
- Docker
- Docker Compose
- Kafka UI
- Zookeeper
Port: 8080
Responsibilities:
- Single entry point for the frontend
- Route requests to microservices
- Validate access-token signature, issuer, audience, type, and required roles
- Apply Redis-backed request rate limiting
Port: 8081
Responsibilities:
- User registration and authentication
- Short-lived access JWT and rotating refresh-token families
- HttpOnly refresh cookie, hashed token persistence, reuse detection, and logout revocation
- Role-based access control
- Provide an internal user snapshot API for service-to-service identity lookups
Database schema:
user_service
Access and refresh tokens have separate responsibilities and signing secrets:
- Access tokens expire after 15 minutes by default and are the only tokens accepted by the API Gateway.
- Refresh tokens expire after 7 days by default and are sent only in an
HttpOnly,SameSitecookie. They are never returned in the JSON body or stored in browser storage. - JWT validation requires the expected signature, issuer, audience,
tokenType, and expiration. Every generated token also carries a unique token ID (jti). - Only a SHA-256 hash of each refresh token is stored in PostgreSQL.
- Every refresh rotates the token. Reusing an already rotated token revokes the complete token family, limiting damage from a stolen token.
- Logout revokes the current refresh-token family and clears the cookie.
- The frontend coalesces concurrent refresh attempts so parallel
401responses do not race the one-time token rotation.
Use different, long random values for JWT_ACCESS_SECRET and JWT_REFRESH_SECRET. For HTTPS production deployments, set AUTH_REFRESH_COOKIE_SECURE=true. Existing PostgreSQL volumes must be upgraded before the new user-service version starts:
./manage.sh migratePort: 8082
Responsibilities:
- Update account balances
- Enforce ownership, account status, currency, balance, and daily-limit invariants
- Own account lookup and beneficiary resolution APIs used by other services
- Resolve beneficiary names through User Service without querying the user schema
- Lock transfer accounts in deterministic order to prevent concurrent deadlocks
- Atomically persist balances, consumer inbox claims, outbox events, and balanced ledger journals
- Store account/transaction audit records in
audit_service.audit_logs
Consumes:
transaction-validatedtransfer-rail-result
Produces:
transaction-checkedfunds-reserved
Database schema:
account_service
For external EFT/FAST transfers, balance is not immediately debited. The amount is added to reserved_balance, which reduces availableBalance. A successful rail result settles the reservation and posts a balanced journal; a rejected result releases both the reservation and the consumed daily limit.
Every posted money movement creates one immutable journal with equal debit and credit totals. The database rejects ledger updates and deletes; no mutation endpoint is exposed.
GET /api/v1/accounts/{accountId}/ledgerlists the authenticated owner's account entries.GET /api/v1/accounts/ledger/transactions/{transactionId}returns both journal sides and calculated debit/credit totals.GET /api/v1/accounts/{accountId}/reconciliationcompares the operational balance with the latest ledger balance.
GET /api/v1/audit-logs?page=0&size=50 is routed through the API Gateway and is restricted to the ADMIN role. Optional actorUsername, action, and resourceType filters are supported. It returns the actor, time, action, account/transaction resource, service, and trusted client IP.
Successful account and transaction reads/creates are recorded automatically. Audit records are append-only at the application level; no update or delete API is exposed.
Port: 8083
Responsibilities:
- Receive transaction requests
- Require an idempotency key and apply type-specific source/target account rules
- Resolve account snapshots and user-owned account IDs through Account Service
- Query only transaction-owned tables when building transaction histories
- Reject same-account transfers, inactive/currency-mismatched accounts, and user-created deposits
- Enforce a locked state machine so duplicate events are no-ops and invalid status regressions fail
- Publish transaction events to Kafka
Produces:
transaction-raw
Database schema:
transaction_service
Port: 8084
Responsibilities:
- Fraud and AML checks
- Detect suspicious transaction behavior
- Validate single transaction amount and frequency
- Track daily account totals atomically and idempotently in Redis
Consumes:
transaction-raw
Produces:
transaction-validated
Database schema:
fraud_service
Port: 8089
Responsibilities:
- Resolve and mask IBAN beneficiaries before submission
- Route internal-bank IBANs to HAVALE and external TRY transfers to the configured FAST/EFT simulation policy
- Consume reserved-fund events and create one idempotent payment attempt per transaction
- Persist only the SHA-256 hash and masked form of the beneficiary IBAN in the rail-attempt table
- Publish successful or rejected rail results through a transactional outbox
Consumes:
funds-reserved
Produces:
transfer-rail-result
Database schema:
payment_rail_service
Port: 8085
Responsibilities:
- Send email, SMS, or push notifications
- Forward notification tasks to RabbitMQ workers
Consumes:
transaction-checked
Produces:
transaction-processedtransaction-completed
Port: 8086
Responsibilities:
- Dashboard and reporting APIs
- Query completed transactions from MongoDB
- Provide analytics and statistics
Port: 8761
Responsibilities:
- Service registration and discovery
- Dynamic routing and load balancing support
transaction-raw → transaction-validated
│
├─ INTERNAL / HAVALE → atomic account + ledger update
│
└─ EFT / FAST → reserve funds → funds-reserved
↓
Payment Rail Service
↓
transfer-rail-result
↓
settle or release funds
↓
transaction-checked → transaction-processed → transaction-completed
Each topic represents a stage of the transaction lifecycle.
The money-movement path uses the Transactional Outbox and Consumer Inbox patterns:
transaction-servicestores the new transaction and itstransaction-rawoutbox event in the same PostgreSQL transaction.account-serviceatomically claims each transaction event inprocessed_events, updates balances, and writes the next outbox event.- EFT/FAST has two independent inbox identities: reservation and settlement. Duplicate delivery at either stage is a no-op.
payment-rail-servicestores the payment attempt and itstransfer-rail-resultoutbox event in one PostgreSQL transaction.- Duplicate Kafka deliveries are ignored by the
(consumer_name, event_id)primary key, so a balance operation is applied once. - Outbox publishers wait for Kafka acknowledgement before marking an event
PUBLISHED. Failed sends stayPENDINGand are retried. - Account consumer failures are retried and then published to
transaction-dlqinstead of being swallowed. - Fraud and transaction-status consumers also propagate failures to Kafka retry/DLQ handling; a logged exception is never treated as successful processing.
Because outbox delivery is intentionally at-least-once, downstream consumers must also be idempotent when they perform non-repeatable side effects.
The money path applies defense in depth in the request DTO, transaction service, account service, and PostgreSQL constraints:
- API-created accounts always start with zero balance. Demo and migrated opening balances receive balanced ledger opening journals.
TRANSFERrequires two different accounts;PAYMENTandWITHDRAWALrequire only a source;DEPOSITrequires only a target and anADMINinitiator.- Both sides of a transfer must be active and use the transaction currency. Cross-currency transfer without an FX leg is rejected.
- Outgoing amounts consume the account's Istanbul-business-day limit in the same transaction as the balance update.
- External transfer amounts are removed from
availableBalanceby a durable reservation before the simulated bank call; a rejection releases the reservation and daily-limit usage. - Turkish IBANs are normalized and verified with MOD-97; newly opened accounts receive checksum-correct IBANs.
- HAVALE to an account inside this platform remains one atomic debit/credit transaction. The demo routes external TRY amounts up to and including 20,000 TRY to FAST and larger amounts to EFT; this is an application simulation rule, not a claimed regulatory limit.
- Ledger posting, balance mutation, consumer inbox claim, and account outbox creation commit or roll back together.
- Transaction statuses follow explicit allowed transitions under a pessimistic database lock. Repeated status events are idempotent.
The account money-transfer path has Testcontainers integration coverage with real PostgreSQL 16 and Kafka containers. The tests verify that:
- a transfer debits and credits the correct accounts;
- duplicate Kafka delivery changes balances only once;
- the consumer inbox and outbox are committed with the balance update;
- the next
transaction-checkedevent is published through Kafka; - insufficient balance rolls back the database transaction and routes the event to
transaction-dlq. - daily-limit violations roll back balance, daily usage, inbox, outbox, and ledger together;
- every transfer produces exactly two immutable ledger entries with equal debit and credit totals;
- client-created deposits, same-account transfers, and out-of-order status regressions are rejected;
- fraud daily rules evaluate an idempotent Redis aggregate rather than one transaction in isolation.
- successful external transfers reserve then settle funds and create a balanced clearing journal;
- rejected external transfers restore available balance and daily-limit usage without creating a ledger posting;
- Payment Rail duplicate events do not create a second attempt or result, and persisted rail records contain no plain IBAN.
Docker must be running to execute the integration suite locally:
mvn -f common-library/pom.xml install
mvn -f account-service/pom.xml verifyThe GitHub Actions workflow in .github/workflows/ci.yml runs all backend tests, the Testcontainers transfer scenarios, the frontend production build, and Docker Compose validation on every push and pull request. Test reports are uploaded as workflow artifacts even when a backend test fails.
Used for operational and transactional data.
Schemas:
user_serviceaccount_servicetransaction_servicefraud_servicepayment_rail_service
These schemas share one PostgreSQL instance in the local Docker topology, but they represent logical ownership boundaries. User, Account, and Transaction domain reads no longer cross those boundaries with SQL joins or native queries; service-owned information is requested through internal APIs. Separate PostgreSQL instances can therefore be introduced later without rewriting these domain queries.
Example tables:
usersrefresh_tokensaccountstransactionsfraud_check_resultsledger_transactionsledger_entriesfund_reservationspayment_rail_attemptsprocessed_eventsoutbox_events
Used to archive completed transactions and support reporting queries.
The archive flow:
transaction-completed
↓
Kafka Connect Sink
↓
MongoDB
All services register themselves in Eureka.
Example configuration:
spring:
application:
name: transaction-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/Example gateway route:
uri: lb://TRANSACTION-SERVICEgit clone <repository-url>
cd fintech-realtime-processingdocker-compose up -dThis starts:
- PostgreSQL
- MongoDB
- Redis
- RabbitMQ
- Kafka
- Zookeeper
- Kafka UI
cd eureka-server
mvn spring-boot:runOpen:
http://localhost:8761
Run the following services in order:
user-servicetransaction-servicefraud-detection-serviceaccount-servicepayment-rail-servicenotification-servicereporting-serviceapi-gateway
Example:
cd transaction-service
mvn spring-boot:runKafka UI:
http://localhost:9090
Eureka Dashboard:
http://localhost:8761
API Gateway:
http://localhost:8080
View infrastructure dashboards
Fraud Detection Service stores expiring velocity counters, atomic daily transaction totals, and processed-event markers that prevent duplicate Kafka events from incrementing an account total twice.
Spring Cloud Gateway uses Redis-backed token buckets to apply per-IP request limits. Token and timestamp keys expire automatically when request traffic stops.
- Distributed transaction management with Saga Pattern
- Extract the shared
audit_serviceschema behind a dedicated Audit Service or Kafka consumer - Extend Consumer Inbox idempotency to notification and reporting side effects
- Operational DLQ replay tooling
- Centralized logging with ELK or Grafana
- OpenTelemetry and tracing
- Resilience4j circuit breaker and retry
- Kubernetes deployment
- AI-based fraud detection model
Developed a real-time fintech transaction platform with Spring Boot microservices, Kafka, PostgreSQL, Redis, MongoDB, RabbitMQ, and Docker. Enforced service-owned domain boundaries by replacing cross-schema User–Account–Transaction queries with internal REST APIs. Implemented transactional outbox/consumer idempotency, secure refresh-token rotation, IBAN-based HAVALE/EFT/FAST orchestration with reserve-settle-release semantics, financial invariants and daily limits, an immutable double-entry ledger, retry/DLQ handling, and Testcontainers end-to-end tests executed in GitHub Actions CI.







