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

#ModuleWhat it does
1Payment OrchestrationFlow Engine — step-by-step payment execution runtime
2Payment ValidationField validation, business rule enforcement, limit checks
3Compliance & ScreeningSanctions screening, AML checks, fraud detection
4Fraud DetectionRisk scoring, velocity checks, pattern analysis
5Fee CalculationDynamic fee rules by payment type, corridor, and amount
6Routing EngineProvider and rail selection with fallback chains
7Approval WorkflowMulti-step, role-based human approval for high-value payments
8Security FrameworkField-level encryption, signing, HMAC, key management
9Integration FabricDynamic HTTP executor with circuit breaker and retry
10Scheme AdaptersRail-specific adapters: SWIFT, ACH, SEPA, TARGET2, CLIQ, CliQ, RTP
11Developer PortalConsumer self-service: API catalog, subscriptions, keys, webhooks
12Experience APIProduct-specific API facades with field masking and simulation
13Operational ConsoleOps search, flow trace, payment timeline, replay, resubmit
14Reconciliation EngineInternal-vs-external statement matching with rule-based auto-match
15Settlement FrameworkBatch creation, net position calculation, settlement confirmation
16Observability EngineMetrics, SLA tracking, alerting, circuit breaker dashboards
17Auto Repair EngineFailure pattern detection, playbook-driven automated retry and repair
18ISO 20022 EngineSchema registry, transformation rules, SWIFT MX generation
19Treasury & LiquidityNostro 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:

  1. country + paymentType + channel + rail
  2. country + paymentType + channel
  3. country + paymentType
  4. paymentType + channel
  5. paymentType only
  6. 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 TypeWhat it does
VALIDATIONField and business rule validation
SCREENINGSanctions and AML screening via port
FRAUD_CHECKFraud risk scoring via port
FEE_CALCULATIONApply fee rules, attach fee to payment
LIMIT_CHECKEnforce daily/monthly/per-transaction limits
APPROVAL_WORKFLOWPause flow, await human approval
ROUTING_DECISIONSelect provider and rail
GATEWAY_SUBMISSIONSubmit to payment scheme adapter
SERVICE_CALLCall an external service from the registry
LIQUIDITY_CHECKCheck nostro account balance before settlement
ISO20022_TRANSFORMTransform payment to ISO 20022 XML format
ISO20022_VALIDATEValidate ISO 20022 message against schema
ISO20022_SENDDispatch ISO 20022 message to network
NOTIFICATIONSend payment status notification
DECISIONConditional branch based on payment data
MANUAL_REPAIRFlag payment for manual intervention
END_SUCCESSTerminal success state
END_FAILURETerminal failure state

Flow States

StateMeaning
RUNNINGActive flow execution in progress
PAUSEDAwaiting approval; payment is PENDING_APPROVAL
COMPLETEDFlow reached END_SUCCESS
FAILEDFlow 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.

FailurePatternDetectorRepairPlaybookServiceRepairQueueServiceRetryOrchestrator → 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:

  1. Parse XML against schema
  2. Extract transactions and map to internal payment records
  3. Update nostro account balance in real time
  4. 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

StoreWhat it holds
PostgreSQLPayments, flow definitions, step executions, config (payment types, rails, services, rules, fees, limits, nostro accounts, reconciliation sessions, settlement batches, compliance cases, repair queue)
MongoDBAudit logs (crypto audit, payment audit), reporting aggregations
RedisDistributed locks (prevent duplicate flow execution), idempotency cache (24-hour key deduplication)
KafkaPaymentMetricEvent 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:

ServiceImageRole
omnyth-backendCustom Spring Boot fat JARAll API endpoints, flow engine, all modules
omnyth-websiteCustom Next.js appMarketing site (omnyth.com)
omnyth-frontendCustom React SPA via nginxPlatform admin UI (app.omnyth.com)
PostgreSQLpostgres:16Primary relational store
MongoDBmongo:7Audit and reporting
Redisredis:7Locking and idempotency
Kafka + Zookeeperconfluentinc/cp-kafkaEvent streaming
Keycloakkeycloak:24OAuth2 identity provider (auth.omnyth.com)
nginxnginx:alpineTLS 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:

  1. Build backend (Maven, skip tests)
  2. Run backend tests (Testcontainers — spins real PostgreSQL and Kafka)
  3. Build frontend (TypeScript compile + Vite)
  4. 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_NEW to 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.