Architecture
Technical architecture reference for OMNYTH Payment Hub. This document covers the system design, all 19 platform modules, the flow execution model, and operational infrastructure.
System Overview
OMNYTH is a modular monolith built with Spring Boot 3.4 and Java 21. 28 Maven submodules map to distinct payment domain concerns and are designed to be extracted into independent microservices. All modules share three core artifacts: domain model, REST API layer, and infrastructure.
The modular monolith approach was chosen to give large-bank deployment teams a single deployable artifact during adoption, while preserving the service boundary discipline needed for future extraction. Each module has clearly defined SPIs (Service Provider Interfaces) that isolate it from infrastructure concerns.
The 19 Platform Modules
| # | Module | What it does |
|---|---|---|
| 1 | Payment Orchestration | Flow Engine — step-by-step payment execution runtime |
| 2 | Payment Validation | Field validation, business rule enforcement, limit checks |
| 3 | Compliance & Screening | Sanctions screening, AML checks, fraud detection |
| 4 | Fraud Detection | Risk scoring, velocity checks, pattern analysis |
| 5 | Fee Calculation | Dynamic fee rules by payment type, corridor, and amount |
| 6 | Routing Engine | Provider and rail selection with fallback chains |
| 7 | Approval Workflow | Multi-step, role-based human approval for high-value payments |
| 8 | Security Framework | Field-level encryption, signing, HMAC, key management |
| 9 | Integration Fabric | Dynamic HTTP executor with circuit breaker and retry |
| 10 | Scheme Adapters | Rail-specific adapters: SWIFT, ACH, SEPA, TARGET2, CLIQ, CliQ, RTP |
| 11 | Developer Portal | Consumer self-service: API catalog, subscriptions, keys, webhooks |
| 12 | Experience API | Product-specific API facades with field masking and simulation |
| 13 | Operational Console | Ops search, flow trace, payment timeline, replay, resubmit |
| 14 | Reconciliation Engine | Internal-vs-external statement matching with rule-based auto-match |
| 15 | Settlement Framework | Batch creation, net position calculation, settlement confirmation |
| 16 | Observability Engine | Metrics, SLA tracking, alerting, circuit breaker dashboards |
| 17 | Auto Repair Engine | Failure pattern detection, playbook-driven automated retry and repair |
| 18 | ISO 20022 Engine | Schema registry, transformation rules, SWIFT MX generation |
| 19 | Treasury & Liquidity | Nostro account management, intraday liquidity, CAMT statement ingest |
Core Trio
Every module depends on three shared artifacts in payment-core/:
- payment-domain — Value objects, domain model (
PaymentInstruction,Money,Party), SPI interfaces, events, and exceptions. No Spring dependencies. - payment-api — REST controllers and request/response DTOs. Depends only on domain.
- payment-infra — All JPA entities, Spring Data repositories, Kafka producers/consumers, Redis services (locking, idempotency). The sole infrastructure implementation layer.
SPI Pattern
Each domain module defines SPI interfaces in a domain/spi/ package. These interfaces represent what the domain needs from infrastructure — persistence, messaging, caching — without depending on it. Implementations live in payment-infra.
This separation is what enables unit testing of business logic without a database, and microservice extraction without touching domain code.
// Domain defines the contract
public interface PaymentRepository {
PaymentInstruction findById(String id);
void save(PaymentInstruction payment);
}
// Infra implements it with JPA
@Repository
public class PaymentRepositoryImpl implements PaymentRepository {
// Spring Data JPA implementation
}Flow Engine
The Flow Engine is the heart of OMNYTH. Every payment is processed by executing a published flow — a configurable sequence of steps stored in the database. There is no hardcoded pipeline.
Flow Selection
The engine selects a flow using a priority cascade:
country + paymentType + channel + railcountry + paymentType + channelcountry + paymentTypepaymentType + channelpaymentTypeonly- No match → payment fails with
NO_ACTIVE_FLOW
Step Execution
Each step has a StepType that maps to a registered executor. Step outputs determine the next step via labeled edges stored as JSON in the step config (_edges key). Edge values are step orderIndex values — not array positions.
// Step config example
{
"threshold": 50000,
"_edges": {
"success": 3,
"failure": 8,
"approval_required": 5
}
}Step Types
| Step Type | What it does |
|---|---|
VALIDATION | Field and business rule validation |
SCREENING | Sanctions and AML screening via port |
FRAUD_CHECK | Fraud risk scoring via port |
FEE_CALCULATION | Apply fee rules, attach fee to payment |
LIMIT_CHECK | Enforce daily/monthly/per-transaction limits |
APPROVAL_WORKFLOW | Pause flow, await human approval |
ROUTING_DECISION | Select provider and rail |
GATEWAY_SUBMISSION | Submit to payment scheme adapter |
SERVICE_CALL | Call an external service from the registry |
LIQUIDITY_CHECK | Check nostro account balance before settlement |
ISO20022_TRANSFORM | Transform payment to ISO 20022 XML format |
ISO20022_VALIDATE | Validate ISO 20022 message against schema |
ISO20022_SEND | Dispatch ISO 20022 message to network |
NOTIFICATION | Send payment status notification |
DECISION | Conditional branch based on payment data |
MANUAL_REPAIR | Flag payment for manual intervention |
END_SUCCESS | Terminal success state |
END_FAILURE | Terminal failure state |
Flow States
| State | Meaning |
|---|---|
RUNNING | Active flow execution in progress |
PAUSED | Awaiting approval; payment is PENDING_APPROVAL |
COMPLETED | Flow reached END_SUCCESS |
FAILED | Flow reached END_FAILURE or threw an unhandled error |
Loop protection: 100 total step executions per run maximum, 10 per individual step. Prevents infinite retry loops in misconfigured flows.
Integration Fabric
External service calls from SERVICE_CALL steps go through the Integration Fabric — a dynamic HTTP executor that reads service configuration from the Service Registry at runtime. No code changes are needed to add a new provider.
All calls go through ResilienceAdapterExecutor (Resilience4j): circuit breaker with configurable failure thresholds, exponential backoff retry, and timeout policies defined per service in application-resilience4j.yml.
Observer Pattern for Compliance
The Compliance module uses a Spring Observer pattern for non-blocking, transactionally-isolated event capture. FraudDecisionObserver and ScreeningResultObserver are SPI interfaces called by the flow engine after each relevant step.
Observer implementations run in REQUIRES_NEW transactions, so a compliance write failure never rolls back the parent payment transaction. Cases are persisted independently.
// Fraud observer SPI
public interface FraudDecisionObserver {
void onFraudDecision(String paymentId, FraudResult result);
}
// Implementation writes a fraud case with REQUIRES_NEW
@Transactional(propagation = REQUIRES_NEW)
public void onFraudDecision(String paymentId, FraudResult result) {
if (result.riskScore() >= threshold) {
fraudCaseRepository.save(new FraudCase(paymentId, result));
}
}Auto Repair Engine
Failed payments are not discarded. The Auto Repair Engine classifies failures by pattern (TIMEOUT, PROVIDER_ERROR, VALIDATION_FAILED, ROUTING_FAILED), matches them to playbooks, and schedules retry attempts with configurable backoff.
FailurePatternDetector → RepairPlaybookService → RepairQueueService→ RetryOrchestrator → re-enters the Flow Engine via FlowEngine.resumeFlow().
Playbooks define: max retry count, backoff strategy (FIXED, EXPONENTIAL, LINEAR), and whether to modify the payment before retry (e.g., strip unsupported fields, change routing).
ISO 20022 Engine
The Messaging Engine handles transformation to and from ISO 20022 XML. It supports:
- Schema Registry — pain.001, pain.002, pacs.008, pacs.002, camt.053, camt.054, and custom extensions
- Transformation Rules — field mapping expressions (JSONPath or XPath) with type casting and conditional logic
- Message Profiles — named configurations that activate specific schemas for specific payment types and rails
- Rail Type Activation — maps rails to message profiles, so SWIFT rails automatically use pacs.008 format
The three ISO 20022 step types (ISO20022_TRANSFORM, ISO20022_VALIDATE,ISO20022_SEND) can be inserted into any flow at any position — they are not special-cased in the engine.
CAMT Statement Ingest
Bank statement files (CAMT.053 end-of-day, CAMT.054 credit/debit notifications) are uploaded via the REST API or the platform UI. The ingest pipeline:
- Parse XML against schema
- Extract transactions and map to internal payment records
- Update nostro account balance in real time
- Seed reconciliation items in the active recon session
This connects the Treasury and Reconciliation modules: every statement upload automatically feeds into the reconciliation engine for matching.
Reconciliation & Settlement
The Reconciliation Engine compares internal payment records against external statements (uploaded via CAMT or entered manually). Match rules are configurable: exact amount + reference, fuzzy reference, amount range.
Matched payments flow into Settlement Batches. The Settlement Framework calculates net positions per counterparty and currency, and produces settlement instructions ready for RTGS or correspondent bank submission.
Observability Pipeline
Every payment flow step fires a PaymentMetricEvent (Kafka topic). The Observability Engine consumes these events to compute:
- Throughput, success rate, error rate — per payment type, rail, and provider
- SLA compliance — actual vs. configured target processing time per payment type
- Circuit breaker state — open/closed/half-open per service integration
- Alert conditions — threshold-based (e.g., error rate > 5% for 5 minutes)
MetricEventService also uses REQUIRES_NEW transactions — metric writes never interfere with payment processing transactions.
Security Architecture
Security is optional and runtime-configurable. No security key material is required at startup. Existing payment flows continue working when no keystore, no key provider, and no active security policy exist.
- Security policies are attached to service integrations, not to payment types
- Keys are referenced by alias — raw key material is never stored in the database
- Crypto operations are logged in an immutable audit trail: SHA-256 hash of input, operation type, result — plaintext is never persisted
- Supported operations: AES-GCM encryption, AES-CBC encryption, JWS RS256 signing, HMAC-SHA256 verification
- IP allowlisting and output field masking are configurable per security policy
Database
| Store | What it holds |
|---|---|
| PostgreSQL | Payments, flow definitions, step executions, config (payment types, rails, services, rules, fees, limits, nostro accounts, reconciliation sessions, settlement batches, compliance cases, repair queue) |
| MongoDB | Audit logs (crypto audit, payment audit), reporting aggregations |
| Redis | Distributed locks (prevent duplicate flow execution), idempotency cache (24-hour key deduplication) |
| Kafka | PaymentMetricEvent stream (consumed by Observability Engine), webhook delivery queue, notification events |
Schema migrations are managed exclusively by Flyway (ddl-auto: none everywhere). Current latest migration: V125. All migrations are in bootstrap/src/main/resources/db/migration/.
Deployment
OMNYTH runs as Docker containers behind nginx on a DigitalOcean droplet. Services:
| Service | Image | Role |
|---|---|---|
| omnyth-backend | Custom Spring Boot fat JAR | All API endpoints, flow engine, all modules |
| omnyth-website | Custom Next.js app | Marketing site (omnyth.com) |
| omnyth-frontend | Custom React SPA via nginx | Platform admin UI (app.omnyth.com) |
| PostgreSQL | postgres:16 | Primary relational store |
| MongoDB | mongo:7 | Audit and reporting |
| Redis | redis:7 | Locking and idempotency |
| Kafka + Zookeeper | confluentinc/cp-kafka | Event streaming |
| Keycloak | keycloak:24 | OAuth2 identity provider (auth.omnyth.com) |
| nginx | nginx:alpine | TLS termination, reverse proxy, routing |
TLS is handled at the nginx layer via Let's Encrypt certificates with auto-renewal. All internal services communicate over the Docker bridge network without TLS.
Authentication Architecture
Platform UI uses Keycloak JS adapter. In local profile, Keycloak is bypassed — all requests are accepted without a token.
API consumers authenticate via OAuth2 client credentials (Keycloak) or API key (issued by Developer Portal). API keys are validated by ApiKeyAuthFilterbefore reaching controllers.
Role-based access: MAKER, CHECKER, APPROVER,ADMIN, CONFIG_MANAGER, SECURITY_ADMIN, OPS_USER. Roles control which nav sections are visible and which API endpoints are accessible.
CI/CD
Bitbucket Pipelines on master branch:
- Build backend (Maven, skip tests)
- Run backend tests (Testcontainers — spins real PostgreSQL and Kafka)
- Build frontend (TypeScript compile + Vite)
- Deploy to DigitalOcean (manual trigger) — tar, upload, build Docker image, restart container
Design Principles
- No hardcoded pipelines — all payment flows are stored in the database and configurable without code changes.
- SPI isolation — domain modules never import from infrastructure. Dependency direction is always domain → SPI ← infra.
- Transaction isolation for observers — compliance and metric writes use
REQUIRES_NEWto prevent interference with payment transactions. - Security is optional — the application starts and processes payments correctly with zero security configuration.
- Fail-safe defaults — circuit breakers default to closed, auto-repair is opt-in per failure pattern, liquidity checks can be configured as soft alerts rather than hard blocks.