Case study 03 · Banking

Rejected transfer. Funds still blocked.

Functional & technical analysis of a critical SEPA transfer flow.

Functional AnalysisBankingSEPAAPIsMicroservicesRoot Cause Analysis
01

Business context

A transfer is a business process, not a single API call.

Allow retail customers to submit SEPA transfers while enforcing balance, daily limits, compliance controls and full auditability.
INV-01

Never transfer more than available funds.

INV-02

Never exceed the daily transfer limit.

INV-03

Rejected transfers must not leave customer funds blocked.

INV-04

Retries must never create duplicate financial effects.

INV-05

Every state transition must be auditable.

02

Banking rules

Financial and system rules govern every transition.

BR-01

Transfer amount must be positive.

BR-02

Available balance must cover the reservation.

BR-03

Projected daily amount must remain within the configured limit.

BR-04

A unique idempotency key is required on submission.

BR-05

Funds are reserved before compliance is completed.

BR-06

If compliance rejects the transfer, reserved funds must be released.

Daily limit
Outgoing10,000
Currency
SupportedEUR
Financial effect
Exactly
03

System architecture

Map every service that participates in one transfer.

01Mobile / Web
02Transfer API
03Limits
04Ledger
05Compliance / AML
06Payment Orchestrator
07SEPA Gateway

Asynchronous recovery path

01Event Bus
02Funds Release Consumer
03Ledger
04

Transfer state machine

Think in states before thinking in screens.

01DRAFT
02VALIDATING
03FUNDS_RESERVED
04COMPLIANCE_CHECK
05SUBMITTED
06SETTLED

Compliance rejection path

01COMPLIANCE_CHECK
02REJECTED
03FUNDS_RELEASE_PENDING
04FUNDS_RELEASED
05

Expected flow

A rejection must restore financial availability.

01Submit transfer
02Validate limit
03Reserve €8,750
04AML review
05Reject transfer
06Publish event
07Release reservation
08Restore €12,480
06

Incident

The transfer is rejected correctly — the customer balance is not.

Transfer
REJECTED

TRF-908771

Ledger
FUNDS_RESERVED

8750.00 still reserved

Available balance
INCORRECT

3730.00

The transfer is REJECTED by compliance, but €8,750 remains reserved and unavailable to the customer.
07

Investigation hypotheses

Build a failure map before touching Postman.

  1. H01The Compliance Service rejected the transfer but did not publish a rejection event
  2. H02The event was published but not delivered to the funds-release queue
  3. H03The Funds Release Consumer rejected the event because of a contract mismatch
  4. H04The Ledger Service processed the release but a stale balance was returned
  5. H05The reservation was duplicated and only one reservation was released
  6. H06A retry created inconsistent transfer and ledger states
08

API evidence

The synchronous transfer flow behaves as designed.

POST /api/v1/transfers · 202 Accepted
{
  "debtorAccountId": "ACC-ES-4410",
  "beneficiaryIban": "DE89370400440532013000",
  "amount": 8750,
  "currency": "EUR",
  "idempotencyKey": "idem-5e71-transfer-8750"
}
VALIDATING
GET /api/v1/transfers/TRF-908771 · 200 OK
{
  "transferId": "TRF-908771",
  "status": "REJECTED",
  "rejectionCode": "AML_REVIEW_FAILED",
  "fundsReleaseStatus": "PENDING"
}
Business rejection
HTTP success and business success are different things.
09

Event evidence

The recovery depends on an asynchronous contract.

transfer.rejected · evt-trf-908771-r1
{
  "eventType": "transfer.rejected",
  "eventVersion": "2.0",
  "eventId": "evt-trf-908771-r1",
  "timestamp": "2026-09-11T15:18:42.413Z",
  "data": {
    "transferId": "TRF-908771",
    "debtorAccountId": "ACC-ES-4410",
    "amount": 8750,
    "currency": "EUR",
    "rejectionCode": "AML_REVIEW_FAILED",
    "releaseFunds": true
  }
}
Funds Release Consumer
Supported
1.0
Received
2.0
Rejected

Unsupported eventVersion: 2.0; expected 1.0

10

Log analysis

Follow transfer state and money state independently.

transfer-processing · production-like trace
TRANSFER_APIPOST /transfers accepted id=TRF-908771 idempotencyKey=idem-5e71-transfer-8750
LIMITSDaily limit check PASS projectedTotal=8750.00 EUR
LEDGERFunds reserved amount=8750.00 availableBalance=3730.00
COMPLIANCEAML screening started transferId=TRF-908771
COMPLIANCETransfer rejected code=AML_REVIEW_FAILED
EVENT_BUSPublished transfer.rejected version=2.0 eventId=evt-trf-908771-r1
SQSDelivered event to funds-release-queue
FUNDS_RELEASEReceived event evt-trf-908771-r1
FUNDS_RELEASEERROR Unsupported eventVersion=2.0 expected=1.0
FUNDS_RELEASEMessage moved to funds-release-dlq
BALANCE_APIGET /accounts/ACC-ES-4410/balance available=3730.00 reserved=8750.00
11

Ledger analysis

Ledger balance and available balance are not the same concept.

Ledger balance
Booked€12,480
Reserved amount
Active€8,750
Available balance
Usable€3,730

A reservation reduces what the customer can use without changing the booked ledger balance.

12

Data validation

Testing the hypothesis at data level

The API and event flow indicated that the transfer had reached a rejected state, but the customer's available balance suggested that the original funds reservation was still active.

At this point, the investigation moves beyond the service response and checks whether the persisted data reflects the expected business state.

The objective is not to query the database for every test, but to use SQL when backend data can confirm or reject a specific investigation hypothesis.

1. Confirm the transfer state and reservation

SQL
SELECT
    t.transfer_id,
    t.status AS transfer_status,
    t.amount,
    r.status AS reservation_status,
    r.reserved_amount
FROM transfers t
LEFT JOIN fund_reservations r
    ON r.transfer_id = t.transfer_id
WHERE t.transfer_id = 'TRF-908771';
Expected investigation result
transfer_status    = REJECTED
reservation_status = ACTIVE
reserved_amount    = 8750.00

This confirms the inconsistency: the business transaction is rejected, but the funds reservation remains active.

2. Verify the customer balance impact

SQL
SELECT
    account_id,
    ledger_balance,
    reserved_amount,
    available_balance
FROM account_balances
WHERE account_id = 'ACC-ES-4410';
Expected result
ledger_balance    = 12480.00
reserved_amount   = 8750.00
available_balance = 3730.00

The ledger balance has not decreased, but the active reservation is still reducing the amount available to the customer.

3. Check whether the release event was processed

SQL
SELECT
    e.event_type,
    e.event_version,
    e.processing_status,
    e.created_at
FROM integration_events e
WHERE e.transfer_id = 'TRF-908771'
ORDER BY e.created_at;
Example result
transfer.created  | 1.0 | PROCESSED
funds.reserved    | 1.0 | PROCESSED
transfer.rejected | 2.0 | FAILED

The data supports the event-flow hypothesis: the rejection event exists, but downstream processing did not complete successfully.

4. Detect the broader pattern

SQL
SELECT
    COUNT(*) AS affected_transfers,
    SUM(r.reserved_amount) AS total_funds_still_reserved
FROM transfers t
JOIN fund_reservations r
    ON r.transfer_id = t.transfer_id
WHERE t.status = 'REJECTED'
  AND r.status = 'ACTIVE';

This query changes the question from “Did one transfer fail?” to “Is this a repeatable systemic state inconsistency?”

SQL is used here as investigation evidence: each query exists to validate a specific hypothesis about state, money or event processing.

13

Root cause

The funds-release service cannot consume the rejection event.

01Compliance rejection ✓
02Transfer REJECTED ✓
03Event publication ✓
04Queue delivery ✓
05Funds Release Consumer ✕
06Funds release — not executed
07Available balance — incorrect
The producer publishes transfer.rejected v2.0, while the Funds Release Consumer supports only v1.0. The message moves to the DLQ and €8,750 remains reserved.
14

Business impact

A technical contract mismatch becomes a customer money problem.

CustomerFunds remain blocked
BalanceAvailable amount is incorrect
OperationsManual reconciliation required
SupportEscalation and explanation required
RiskDuplicate transfer attempts
TrustCustomer cannot access their money
15

Defect

Translate distributed-system evidence into banking risk.

IDBNK-4412
TitleReserved funds are not released after compliance rejection due to incompatible transfer.rejected event version
SeverityCritical
ExpectedTransfer REJECTED → reserved funds released → available balance restored.
ActualTransfer REJECTED → event moves to DLQ → €8,750 remains reserved.
Customer impactCustomer cannot access money that should have been released after rejection.
Operational impactManual reconciliation, support escalation and DLQ replay may be required.
16

Regression strategy

Turn the incident into durable quality coverage.

Contract tests

Verify transfer.rejected producer and consumer compatibility before deployment.

Integration tests

Reserve → reject → event → consume → release funds.

State tests

Validate legal transfer and funds-release transitions.

Idempotency tests

Duplicate requests and events create one financial effect.

Concurrency tests

Competing reservations cannot overdraw available balance.

Monitoring

Alert on DLQ growth, stuck reservations and reconciliation mismatches.

Postman collection
PassPOST /transfers · 202 Accepted
PassGET /transfers/TRF-908771 · REJECTED
FailGET /accounts/ACC-ES-4410/balance · reserved 8750
PassRetry · same Idempotency-Key
17

UAT

Validate the customer and operational outcome — not only the services.

UAT-01

Valid transfer below daily limit

Funds reserved once; transfer progresses to SUBMITTED.

UAT-02

Transfer exceeds available balance

Rejected before reservation; balance unchanged.

UAT-03

Transfer exceeds daily limit

Rejected with clear reason; no financial effect.

UAT-04

Compliance rejects after reservation

Transfer REJECTED and full reservation released.

UAT-05

Duplicate submit with same idempotency key

Same transfer returned; no duplicate reservation.

UAT-06

Rejection event delivered twice

Funds released exactly once.

UAT-07

Consumer unavailable temporarily

Event safely retried; reservation eventually released.

UAT-08

Message reaches DLQ

Alert raised and operational replay path available.

Gherkin evidence
Feature: SEPA transfer validation and funds release
  As a retail banking customer
  I want transfers and rejected transfers to update my available balance correctly
  So that my money is never duplicated, overdrawn or blocked incorrectly

  Scenario: Compliance rejects after reservation
    Given 8750 EUR has been reserved
    When compliance rejects the transfer
    And a compatible transfer.rejected event is processed
    Then the transfer should be REJECTED
    And the reservation should be released
    And the available balance should return to 12480 EUR

  Scenario: Funds-release consumer cannot process contract version
    Given a transfer.rejected event with eventVersion 2.0
    And the consumer supports only eventVersion 1.0
    When the event is consumed
    Then the event should be rejected
    And the message should be sent to a Dead Letter Queue
    And an operational alert should be raised
18

Risk analysis

Financial systems fail in the edges between valid states.

ScenarioFailure modeRiskExpected control
Duplicate submissionTwo user clicks create two transfer attemptsHIGHIdempotency key must return the original transfer instead of creating another
Concurrent transfersTwo valid transfers jointly exceed available balanceCRITICALReservation must be atomic against available balance
Compliance rejectionTransfer rejected after funds reservationCRITICALReserved funds must be released exactly once
Provider timeoutSEPA gateway accepts request but HTTP response is lostHIGHReconcile using provider reference before retrying
Out-of-order eventRelease event arrives before local state updateHIGHConsumer must handle or safely retry transient state mismatch
Duplicate eventSame rejection event delivered twiceHIGHFunds release must be idempotent
DLQ accumulationConsumer rejects valid business eventsCRITICALMonitoring and replay procedure required
Stale readLedger is correct but app shows old balanceMEDIUMRead consistency and cache invalidation must be defined
Recovery controlsRetriesDLQReconciliationAuditabilityEvent orderingDuplicate events
19

Test strategy

Deciding where quality needs to be proven

Not every risk requires the same type of test.

For a critical banking flow, the strategy should connect business risk with the level at which the behaviour can be validated most effectively.

RiskPrimary validation levelWhyPriority
Transfer amount exceeds daily limitAPI / serviceBusiness rule can be validated directly and deterministically without UI dependency.High
Funds are not reserved before complianceIntegration + dataRequires validation of orchestration and persisted financial state.Critical
Rejected transfer keeps funds reservedIntegration + database + E2EThe risk spans business state, asynchronous processing and customer-visible balance.Critical
Duplicate transfer submissionAPI + integrationIdempotency must be validated at the transaction boundary and downstream processing.Critical
Out-of-order or incompatible eventsContract + integrationThe failure occurs between services rather than in the UI.Critical
Customer sees incorrect final stateE2EThe complete user journey must reflect the final financial state correctly.High

Coverage decisions

UI

Validate: transfer initiation, user-facing status, validation messages, final balance/state presentation.

Do not rely on UI alone for: orchestration, event delivery, reservation persistence, idempotency.

API / service

Validate: business rules, limits, validation responses, transaction state changes, duplicate requests, error handling.

Integration / contract

Validate: service-to-service behaviour, event schema compatibility, event versions, async processing, retries, DLQ behaviour.

Database / data

Validate: persisted transfer state, active/released reservations, balance consistency, reconciliation conditions, unexpected state combinations.

End-to-end

Validate: critical customer journeys, financial state consistency, rejected/failed transaction recovery, final user-visible outcome.

Entry and exit thinking

Entry conditions

  • Business rules understood
  • Critical integrations available
  • Test data prepared
  • Event contracts known
  • Environment stable enough for meaningful execution

Exit conditions

  • No open Critical defects in the transfer lifecycle
  • No known scenario where a REJECTED transfer can leave funds reserved
  • Critical API/integration/E2E scenarios pass
  • Duplicate and retry behaviour validated
  • Financial state reconciles correctly after rejection/failure
  • No unexplained DLQ messages in the critical flow

For this flow, release readiness is defined by business-state consistency, not simply by the percentage of test cases that pass.

20

Analysis outcome

From transaction failure to systemic risk

Critical business invariant identified

A rejected transfer must never leave the customer's funds unavailable.

Financial inconsistency traced

The transfer reached REJECTED, while €8,750 remained reserved and continued reducing the customer's available balance.

Root cause isolated

The transfer.rejected event was produced using contract v2.0, while the Funds Release Consumer only supported v1.0.

Failure propagation understood

The analysis connected the event incompatibility with the DLQ, the missing funds-release operation and the resulting ledger inconsistency.

Systemic risk identified

The issue was not limited to one transaction: any rejected transfer following the same event path could potentially leave funds reserved.

Quality controls defined

Contract validation, reconciliation, DLQ monitoring, idempotency, concurrency testing and end-to-end regression were identified as controls for the critical flow.

Conclusion

The objective was not only to find where the transaction failed, but to understand why the system allowed an invalid business state to exist.

21

Takeaway

In banking, quality includes the integrity of money, state and recovery.

The critical question is not only “Did the API respond correctly?” It is “Are transfer state, ledger state, business rules and customer-visible balance still consistent after every possible outcome?”