This document describes the technical design for the debit card transaction processing feature. The system receives debit card authorization requests from a payment network, validates and routes them through an authorization pipeline, maintains accurate account balance states via authorization holds, persists a full audit trail, and delivers real-time cardholder notifications.
The design follows a service-oriented architecture where four logical components — Transaction_Processor, Authorization_Engine, Account_Service, and Notification_Service — communicate through well-defined interfaces. The system must be correct (no double-charges, no phantom holds), consistent (Available_Balance invariant always holds), and idempotent (duplicate submissions are safe).
- Correctness: Every balance mutation must be atomic; partial updates must be rolled back or flagged for reconciliation.
- Idempotency: Any transaction replayed with the same Idempotency_Key must return the original result without side effects.
- Auditability: Every state transition of a Transaction_Record is persisted durably before it takes effect.
- Resilience: Transient failures in downstream services (Account_Service, Notification_Service) are handled with bounded retries; irrecoverable failures are routed to manual reconciliation.
- Latency: Authorization decisions are returned within 2000 ms; cardholder notifications are delivered within 5 seconds.
The system is structured as a pipeline of loosely coupled services. An incoming debit card authorization request enters through the Transaction_Processor, which acts as the primary orchestrator.
flowchart TD
PN[Payment Network] -->|Authorization Request| TP[Transaction_Processor]
TP -->|Validate & persist PENDING| DB[(Transaction Store)]
TP -->|Authorize| AE[Authorization_Engine]
AE -->|Check account state & balance| AS[Account_Service]
AS -->|Read/write balances & holds| ACDB[(Account Store)]
AE -->|Place hold| AS
AE -->|Approval / Decline| TP
TP -->|Update record| DB
TP -->|Notify| NS[Notification_Service]
NS -->|Alert| CH[Cardholder]
subgraph Settlement Flow
SF[Settlement Event] -->|Post transaction| TP2[Transaction_Processor]
TP2 -->|Release hold + debit ledger| AS
TP2 -->|Update record to POSTED| DB
end
subgraph Reversal Flow
RF[Reversal Request] -->|Reverse transaction| TP3[Transaction_Processor]
TP3 -->|Release hold or credit ledger| AS
TP3 -->|Update record to REVERSED| DB
end
| Component | Responsibility |
|---|---|
| Transaction_Processor | Entry point; validates input, manages idempotency, orchestrates the pipeline, persists all state transitions |
| Authorization_Engine | Evaluates approval/decline decisions based on account state and business rules |
| Account_Service | Owns the Account Store; manages Available_Balance, Ledger_Balance, and Authorization_Holds atomically |
| Notification_Service | Delivers cardholder alerts asynchronously with retry logic |
- Transaction_Processor ↔ Authorization_Engine: Synchronous request/response (bounded by 2000 ms SLA).
- Transaction_Processor ↔ Account_Service: Synchronous for balance reads and hold operations (within the authorization path); synchronous for settlement/reversal operations.
- Transaction_Processor → Notification_Service: Asynchronous; notifications are fire-and-forget from the authorization path perspective, with the Notification_Service owning retry behavior.
- All persistence operations: Write-ahead; a record is persisted before any downstream action that depends on it.
The Transaction_Processor is the sole entry point for external transaction events (authorization requests, settlement events, reversal requests).
POST /transactions/authorize
Request:
card_number: string (required)
amount: decimal (required, > 0)
currency: string (required, 3-letter ISO 4217)
merchant_id: string (required)
timestamp: ISO 8601 datetime (required)
idempotency_key: string (optional; assigned if absent)
Response (success):
transaction_id: string
status: "APPROVED" | "DECLINED"
approval_code: string (present when APPROVED)
decline_reason: string (present when DECLINED)
available_balance: decimal (present when APPROVED)
Response (error):
error_code: string
error_detail: string (list of invalid/missing fields)
POST /transactions/{transaction_id}/settle
Request:
settlement_amount: decimal (required, > 0)
settlement_timestamp: ISO 8601 datetime (required)
Response:
transaction_id: string
status: "POSTED"
POST /transactions/{transaction_id}/reverse
Request:
reason: string (optional)
Response (success):
transaction_id: string
status: "REVERSED"
Response (error):
error_code: "TRANSACTION_NOT_FOUND" | "ALREADY_REVERSED" | "REVERSAL_NOT_PERMITTED" | "REVERSAL_FAILED"
current_status: string (present for REVERSAL_NOT_PERMITTED)
The Transaction_Processor applies the following validation checks in order before forwarding to the Authorization_Engine:
- All required fields present (
card_number,amount,currency,merchant_id,timestamp). amountis a positive numeric value (> 0).currencymatches the ISO 4217 three-letter format (e.g.,USD,EUR).timestampis a valid ISO 8601 datetime.- Idempotency check: if
idempotency_keymatches a known record, return stored result. - Card lookup:
card_numbermaps to an active checking account; otherwise reject withCARD_NOT_FOUND. - Persist
PENDINGTransaction_Record before forwarding.
The Authorization_Engine receives a validated transaction and an account reference, then produces an approval or decline decision.
authorize(account_id, amount, transaction_id) -> AuthorizationResult
AuthorizationResult:
decision: "APPROVED" | "DECLINED"
approval_code: string (present when APPROVED)
decline_reason: "INSUFFICIENT_FUNDS" | "ACCOUNT_UNAVAILABLE" |
"SERVICE_UNAVAILABLE" | "HOLD_PLACEMENT_FAILED"
available_balance: decimal (present when APPROVED)
- Retrieve account status from Account_Service. If Account_Service is unavailable → decline
SERVICE_UNAVAILABLE. - If account is
FROZENorSUSPENDED→ declineACCOUNT_UNAVAILABLE(no balance check). - Retrieve Available_Balance. If balance retrieval fails → decline
SERVICE_UNAVAILABLE. - If
amount > Available_Balance→ declineINSUFFICIENT_FUNDS. - If account status is not
ACTIVEat this point → declineACCOUNT_UNAVAILABLE(guard: onlyACTIVEaccounts proceed to hold placement). - Instruct Account_Service to place Authorization_Hold. If hold placement fails → decline
HOLD_PLACEMENT_FAILED. - Return
APPROVEDwith approval code and updated Available_Balance.
The Account_Service owns all balance state. All mutations are atomic — no partial updates are externally visible.
getAccountStatus(account_id) -> AccountStatus
status: "ACTIVE" | "FROZEN" | "SUSPENDED"
getAvailableBalance(account_id) -> BalanceResult
available_balance: decimal
placeHold(account_id, hold_id, amount) -> HoldResult
success: boolean
releaseHold(account_id, hold_id) -> ReleaseResult
success: boolean
settleTransaction(account_id, hold_id, settled_amount) -> SettleResult
success: boolean
creditLedger(account_id, amount) -> CreditResult
success: boolean
The Account_Service MUST maintain at all times:
Available_Balance = Ledger_Balance − Σ(active Authorization_Hold amounts)
Hold placement reduces Available_Balance atomically. Settlement atomically releases the hold and reduces Ledger_Balance by the settled amount, then recomputes Available_Balance. Hold expiry (7 days) or reversal atomically releases the hold and restores Available_Balance without touching Ledger_Balance.
The Notification_Service delivers cardholder alerts asynchronously.
sendAlert(cardholder_id, alert_type, payload) -> DeliveryReceipt
alert_type: "APPROVED" | "DECLINED"
Approved payload:
merchant_name: string
amount: decimal
available_balance: decimal
Declined payload:
merchant_name: string
amount: decimal
decline_reason: string
- Await delivery confirmation within 30 seconds of each attempt.
- On failure, retry after 30 seconds, up to 3 total attempts.
- On exhaustion of retries, log failure with
transaction_idand attempt count; do not retry further. - If the cardholder has disabled notifications, suppress the alert immediately without attempting delivery.
TransactionRecord {
transaction_id: UUID // system-assigned
idempotency_key: string // caller-supplied or system-assigned
card_number: string // hashed/tokenized for storage
account_id: UUID // resolved from card_number
merchant_id: string
merchant_name: string
amount: decimal(19,4)
currency: ISO 4217 code // e.g., "USD"
status: TransactionStatus
approval_code: string? // set on APPROVED
decline_reason: string? // set on DECLINED
settlement_amount: decimal(19,4)? // set on POSTED
reversal_reason: string? // set on REVERSED
created_at: timestamp // when PENDING record was created
authorized_at: timestamp? // when status changed to APPROVED/DECLINED
posted_at: timestamp? // when status changed to POSTED
reversed_at: timestamp? // when status changed to REVERSED
hold_id: UUID? // reference to Authorization_Hold
reconciliation_flag: boolean // true if flagged for manual reconciliation
}
TransactionStatus: PENDING | APPROVED | DECLINED | POSTED | REVERSED
Status transition diagram:
stateDiagram-v2
[*] --> PENDING: receipt
PENDING --> APPROVED: authorization success
PENDING --> DECLINED: authorization failure
APPROVED --> POSTED: settlement
APPROVED --> REVERSED: reversal request
POSTED --> REVERSED: reversal request
Terminal statuses: APPROVED, DECLINED, POSTED, REVERSED
(APPROVED is terminal from an idempotency perspective but can still transition to POSTED or REVERSED.)
AuthorizationHold {
hold_id: UUID
account_id: UUID
transaction_id: UUID
amount: decimal(19,4)
status: HoldStatus // ACTIVE | RELEASED
placed_at: timestamp
released_at: timestamp?
expires_at: timestamp // placed_at + 7 calendar days
}
HoldStatus: ACTIVE | RELEASED
Account {
account_id: UUID
card_number: string // tokenized
status: AccountStatus // ACTIVE | FROZEN | SUSPENDED
ledger_balance: decimal(19,4)
available_balance: decimal(19,4) // derived: ledger_balance − Σ active holds
active_holds: AuthorizationHold[]
}
IdempotencyRecord {
idempotency_key: string // unique index
transaction_id: UUID
result_status: TransactionStatus
result_payload: JSON // full response captured at resolution
created_at: timestamp
expires_at: timestamp // created_at + 24 hours
}
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
For any transaction missing one or more required fields, or containing an amount ≤ 0, or containing a currency code that is not a 3-letter alphabetic string, the Transaction_Processor SHALL reject the transaction with an appropriate error and SHALL NOT create a Transaction_Record or forward the transaction to the Authorization_Engine.
Validates: Requirements 1.1, 1.2, 1.3
For any transaction whose card number does not map to an active checking account, the Transaction_Processor SHALL reject it with CARD_NOT_FOUND and SHALL NOT forward it to the Authorization_Engine.
Validates: Requirements 1.5
For any transaction that has reached a terminal status within the past 24 hours, resubmitting it with the same Idempotency_Key SHALL return the identical status and result payload without creating a new Transaction_Record or mutating any account balance.
Validates: Requirements 1.6, 6.2, 6.3
For any account, at any point in time, Available_Balance SHALL equal Ledger_Balance minus the sum of all active Authorization_Hold amounts.
Validates: Requirements 3.1, 3.2, 3.3, 3.4
For any transaction where amount > Available_Balance at the time of authorization, the Authorization_Engine SHALL decline the transaction with INSUFFICIENT_FUNDS and SHALL NOT place an Authorization_Hold.
Validates: Requirements 2.3
For any transaction submitted against a frozen or suspended account, the Authorization_Engine SHALL decline it with ACCOUNT_UNAVAILABLE without performing a balance check or placing a hold.
Validates: Requirements 2.4
For any approved transaction, a successful hold placement SHALL reduce Available_Balance by exactly the transaction amount, leaving Ledger_Balance unchanged. If hold placement fails, the transaction SHALL be declined with HOLD_PLACEMENT_FAILED and Available_Balance SHALL be unchanged.
Validates: Requirements 2.2, 3.1
For any settled transaction, after settlement the Ledger_Balance SHALL be reduced by the settled amount, the Authorization_Hold SHALL be released, and Available_Balance SHALL equal the updated Ledger_Balance minus the sum of all remaining active holds.
Validates: Requirements 3.2
For any hold that is released (via reversal or 7-day expiry), the Available_Balance SHALL be restored by the hold amount and the Ledger_Balance SHALL remain unchanged.
Validates: Requirements 3.3
For any transaction, the Transaction_Processor SHALL persist a PENDING Transaction_Record before forwarding to the Authorization_Engine. No status transition resulting from authorization, settlement, or reversal SHALL be visible without a durable write preceding it.
Validates: Requirements 4.1, 4.6
For any reversal request referencing a Transaction_Record that is already REVERSED, the system SHALL return ALREADY_REVERSED. For a record that is PENDING or DECLINED, the system SHALL return REVERSAL_NOT_PERMITTED. For a non-existent transaction, it SHALL return TRANSACTION_NOT_FOUND. In all these cases, no balance mutation SHALL occur.
Validates: Requirements 7.3, 7.4, 7.5
For any set of concurrent transactions against the same account, the final Available_Balance SHALL reflect the cumulative effect of each transaction's full authorization, as if they were processed sequentially; no two transactions SHALL race to consume the same balance.
Validates: Requirements 6.4
For any approved transaction, the cardholder alert SHALL contain the merchant name, transaction amount, and updated Available_Balance. For any declined transaction, the alert SHALL contain the merchant name, transaction amount, and decline reason code.
Validates: Requirements 5.1, 5.2
| Condition | Response |
|---|---|
| Missing required field | VALIDATION_ERROR with list of missing/invalid fields |
| Amount ≤ 0 | VALIDATION_ERROR — amount invalid |
| Invalid currency code | VALIDATION_ERROR — currency invalid |
| Card not found | CARD_NOT_FOUND |
| Initial record persist fails | PROCESSING_ERROR — transaction rejected |
| Condition | Decline Code |
|---|---|
| Account frozen or suspended | ACCOUNT_UNAVAILABLE |
| Insufficient Available_Balance | INSUFFICIENT_FUNDS |
| Account_Service unavailable | SERVICE_UNAVAILABLE |
| Hold placement failure | HOLD_PLACEMENT_FAILED |
| Condition | Error Code |
|---|---|
| Transaction not found | TRANSACTION_NOT_FOUND |
| Transaction already reversed | ALREADY_REVERSED |
| Transaction in PENDING or DECLINED state | REVERSAL_NOT_PERMITTED |
| Account_Service fails to release hold or credit ledger | REVERSAL_FAILED |
- Account_Service balance operations (hold, release, settle, credit): Retry up to 3 times on transient failure. On exhaustion, flag
Transaction_Record.reconciliation_flag = true, do not partially apply changes, and halt all further automated processing for that transaction until manual intervention resolves the flagged record. - Transaction_Record status updates: Retry up to 3 times on transient failure for any post-authorization status update (approved or declined). On exhaustion, flag for manual reconciliation without rolling back the authorization.
- Notification delivery: Retry up to 3 total attempts, waiting 30 seconds between each. On exhaustion, log and stop.
- Authorization_Engine timeout: If the full authorization cycle exceeds 2000 ms, the Authorization_Engine declines with
SERVICE_UNAVAILABLE.
No partial state is committed externally. If a multi-step operation (e.g., release hold + update ledger) cannot complete atomically, the entire operation is either retried or flagged for reconciliation. At no point is the Available_Balance invariant violated by a half-applied update visible to callers.
The testing strategy combines example-based unit tests for specific scenarios with property-based tests for universal correctness guarantees.
Unit / Integration Tests cover:
- Specific valid and invalid authorization request examples
- Each decline reason code triggered by its exact condition
- Status transition sequences (PENDING → APPROVED → POSTED, etc.)
- Notification suppression for opted-out cardholders
- Retry exhaustion paths routing to reconciliation
- Settlement and reversal happy paths and error cases
Property-Based Tests cover the correctness properties enumerated in the Correctness Properties section. Each property test runs a minimum of 100 iterations using randomly generated inputs.
Library selection: Use fast-check (TypeScript/JavaScript), hypothesis (Python), or jqwik (Java/Kotlin) depending on the implementation language.
Generator design:
arbTransaction: generates transactions with arbitrary card numbers, amounts, currencies, and merchants — including invalid variants (negative amounts, missing fields, malformed currency codes).arbAccount: generates account states with arbitrary ledger balances, hold sets, and statuses (ACTIVE, FROZEN, SUSPENDED).arbHoldSet: generates sets of Authorization_Hold records with varying amounts and statuses.arbIdempotencyKey: generates strings that may or may not collide with existing keys.
Tag format: Each property-based test MUST include a reference comment:
// Feature: debit-card-transaction-processing, Property {N}: {property_text}
Minimum iteration count: 100 iterations per property test.
Mocking strategy: Account_Service and Notification_Service are replaced with in-memory fakes for property tests. This keeps tests fast, deterministic for replays, and free of external I/O costs while still exercising the full logic of Transaction_Processor and Authorization_Engine.
| Area | Approach |
|---|---|
| Input validation (Req 1) | Property 1 (PBT) + example unit tests per field |
| Card lookup (Req 1) | Property 2 (PBT) |
| Idempotency (Req 1, 6) | Property 3 (PBT) |
| Authorization decisions (Req 2) | Properties 5, 6, 7 (PBT) |
| Balance invariant (Req 3) | Property 4 (PBT) |
| Settlement correctness (Req 3) | Property 8 (PBT) |
| Hold expiry/reversal (Req 3) | Property 9 (PBT) |
| Record durability (Req 4) | Property 10 (PBT with in-memory write-ahead log) |
| Reversal safety (Req 7) | Property 11 (PBT) |
| Concurrency (Req 6.4) | Property 12 (concurrency PBT with parallel runners) |
| Notification content (Req 5) | Property 13 (PBT) |
| Retry / reconciliation (Req 3.5, 4.7) | Integration tests with simulated failures |
| Latency SLAs (Req 2.5, 2.6, 5.1, 5.2) | Performance integration tests |
https://kiro.dev/ demo - it generates the spec and asks questions.
then you can have it generate tech design, analyze requirements for bugs (maybe should have done first), and generate task list.
"user has a checking account with a debit card. process incoming debit card transactions."
https://gist.github.com/dgobaud/30acb5b637a097231d97f7ef93d63fc4
SPEC GENERATION
REQUIREMENTS ANALYSIS
GENERATE TASK LIST