Executive Summary
Mobile money platforms built on USSD/SMS inherit a fundamental weakness from the telecom layer they run on: the telco's Session-ID was never designed to be a reliable transaction boundary. This document explains why that weakness exists, and describes a Dynamic Token-Exchange Protocol that removes the telco session from the transaction-integrity equation entirely, replacing it with an application-owned, single-use, atomically-verified token backed by a clustered Redis store.
The design deliberately balances two competing architectural instincts. Pure microservice decomposition would split PIN authentication, token issuance, transfer execution, and ledger posting into independently deployable services — good for team autonomy and independent scaling, but dangerous for a workload where a network partition between two services could leave money in an inconsistent state. Pure monolith would simplify consistency but make it harder to scale stateless, high-volume components (authentication, notifications) independently from the financially sensitive core. This design therefore uses a hybrid topology: a small number of stateless, horizontally-scaled microservices at the edge (Auth, Notification), and a modular monolith — the Transaction Engine — for everything that must share a single unit of transactional consistency (token verification, ledger debit/credit, audit write).
The remainder of this document walks through the problem, the protocol, the diagrams, the implementation, and the operational characteristics (capacity, failure handling, security, observability) needed to take this design into production.
1. Introduction & Scope
1.1 Purpose
The purpose of this design is to define a transaction-processing architecture for a mobile money service that (a) tolerates the unreliability of the underlying USSD/SMS transport, (b) guarantees that no financial transaction is ever double-executed regardless of client or network retries, and (c) remains simple enough to reason about and audit, given that correctness failures in this domain directly translate to financial loss.
1.2 Scope & Non-Goals
In scope: PIN-based subscriber authentication over USSD, token issuance and atomic redemption, peer-to-peer transfer execution against a relational ledger, audit logging, and the operational concerns (scaling, resilience, security, observability) needed to run the flow described in Sections 5 and 6.
Out of scope: Know-Your-Customer (KYC) onboarding, agent cash-in/cash-out float management, cross-border settlement/interoperability (e.g. with card networks or other mobile money operators), regulatory reporting pipelines, and the USSD menu-rendering layer itself. These are assumed to be separate subsystems that consume the transaction primitives defined here.
1.3 Key Actors
These targets drive several of the design decisions in later sections (in particular the choice of Redis for transient state, and pessimistic locking for the ledger).
Mobile money platforms built on USSD/SMS inherit a fundamental weakness from the telecom layer they run on: the telco's Session-ID was never designed to be a reliable transaction boundary. This document explains why that weakness exists, and describes a Dynamic Token-Exchange Protocol that removes the telco session from the transaction-integrity equation entirely, replacing it with an application-owned, single-use, atomically-verified token backed by a clustered Redis store.
The design deliberately balances two competing architectural instincts. Pure microservice decomposition would split PIN authentication, token issuance, transfer execution, and ledger posting into independently deployable services — good for team autonomy and independent scaling, but dangerous for a workload where a network partition between two services could leave money in an inconsistent state. Pure monolith would simplify consistency but make it harder to scale stateless, high-volume components (authentication, notifications) independently from the financially sensitive core. This design therefore uses a hybrid topology: a small number of stateless, horizontally-scaled microservices at the edge (Auth, Notification), and a modular monolith — the Transaction Engine — for everything that must share a single unit of transactional consistency (token verification, ledger debit/credit, audit write).
The remainder of this document walks through the problem, the protocol, the diagrams, the implementation, and the operational characteristics (capacity, failure handling, security, observability) needed to take this design into production.
1. Introduction & Scope
1.1 Purpose
The purpose of this design is to define a transaction-processing architecture for a mobile money service that (a) tolerates the unreliability of the underlying USSD/SMS transport, (b) guarantees that no financial transaction is ever double-executed regardless of client or network retries, and (c) remains simple enough to reason about and audit, given that correctness failures in this domain directly translate to financial loss.
1.2 Scope & Non-Goals
In scope: PIN-based subscriber authentication over USSD, token issuance and atomic redemption, peer-to-peer transfer execution against a relational ledger, audit logging, and the operational concerns (scaling, resilience, security, observability) needed to run the flow described in Sections 5 and 6.
Out of scope: Know-Your-Customer (KYC) onboarding, agent cash-in/cash-out float management, cross-border settlement/interoperability (e.g. with card networks or other mobile money operators), regulatory reporting pipelines, and the USSD menu-rendering layer itself. These are assumed to be separate subsystems that consume the transaction primitives defined here.
1.3 Key Actors
- Subscriber — the end user, identified by MSISDN (mobile number), interacting via a feature phone or basic handset using USSD short codes (e.g. *120#).
- USSD/SMS Gateway — the mobile network operator's (MNO) infrastructure that terminates the USSD session and forwards HTTP requests to the platform. Treated as an untrusted, unreliable transport in this design.
- Transaction Engine — the application tier described in this document; owns authentication, token lifecycle, and ledger mutation.
- Operations / Fraud team — consumers of the audit trail and observability signals produced by the platform.
These targets drive several of the design decisions in later sections (in particular the choice of Redis for transient state, and pessimistic locking for the ledger).
2. Architectural Overview & The Core Flaw of Standard USSD
Traditional USSD application architectures rely on the telecommunication gateway's Session-ID to maintain session flow and idempotency. This pattern introduces significant architectural vulnerabilities, each of which is expanded below with the concrete failure mechanism and its downstream consequence.
2.1 Why Session-ID-based idempotency fails1. Session Mutation
Telco gateways regularly drop connections, restart, or re-route packets under load. Because the Session-ID is generated and owned entirely by infrastructure outside the platform's control, it can change or be duplicated mid-interaction without any signal reaching the application. A design that keys transactional state off this identifier is implicitly trusting a value it cannot verify, refresh, or invalidate on its own terms.
2. Double-Spend Race ConditionsIf a user submits a "Confirm Payment" request and a network timeout occurs, the handset, the gateway, or an intermediate SMSC may retry the transaction under a new session context. If the server-side duplicate-detection logic keys off Session-ID (or worse, off nothing at all), the retry is indistinguishable from a legitimate new request. The result is a second debit for a transaction the subscriber believes already failed — the single most damaging failure mode a payments system can have.
3. Implicit Over-Trust
If the API endpoint implicitly trusts a transient session state without validating a cryptographic proof of the user's intent at each step, it exposes the system to menu-bypass or session-hijacking attacks — for example, an attacker replaying a captured "confirm transfer" request against a still-open session for a different amount or recipient.
4. Telco Session-Timeout Variance
MNOs configure USSD session timeouts independently, and these values are neither published reliably nor guaranteed to be consistent across roaming partners. An architecture that assumes a fixed session lifetime will silently misbehave on networks that expire sessions earlier (truncated transactions) or later (stale sessions that appear valid long after the user has abandoned the flow).
2.2 Session-ID vs. Token-Based Idempotency
Traditional USSD application architectures rely on the telecommunication gateway's Session-ID to maintain session flow and idempotency. This pattern introduces significant architectural vulnerabilities, each of which is expanded below with the concrete failure mechanism and its downstream consequence.
2.1 Why Session-ID-based idempotency fails1. Session Mutation
Telco gateways regularly drop connections, restart, or re-route packets under load. Because the Session-ID is generated and owned entirely by infrastructure outside the platform's control, it can change or be duplicated mid-interaction without any signal reaching the application. A design that keys transactional state off this identifier is implicitly trusting a value it cannot verify, refresh, or invalidate on its own terms.
2. Double-Spend Race ConditionsIf a user submits a "Confirm Payment" request and a network timeout occurs, the handset, the gateway, or an intermediate SMSC may retry the transaction under a new session context. If the server-side duplicate-detection logic keys off Session-ID (or worse, off nothing at all), the retry is indistinguishable from a legitimate new request. The result is a second debit for a transaction the subscriber believes already failed — the single most damaging failure mode a payments system can have.
3. Implicit Over-Trust
If the API endpoint implicitly trusts a transient session state without validating a cryptographic proof of the user's intent at each step, it exposes the system to menu-bypass or session-hijacking attacks — for example, an attacker replaying a captured "confirm transfer" request against a still-open session for a different amount or recipient.
4. Telco Session-Timeout Variance
MNOs configure USSD session timeouts independently, and these values are neither published reliably nor guaranteed to be consistent across roaming partners. An architecture that assumes a fixed session lifetime will silently misbehave on networks that expire sessions earlier (truncated transactions) or later (stale sessions that appear valid long after the user has abandoned the flow).
2.2 Session-ID vs. Token-Based Idempotency
3. The Solution: Dynamic Token-Exchange Protocol
Our architecture resolves these vulnerabilities by utilizing a transient, single-use, application-driven Transaction Token mapped directly to the subscriber's MSISDN within an atomic memory fabric (a clustered, highly-available Redis deployment). The protocol is built on three design principles.
3.1 Design Principles
Explicit Handshake
Transactions are decoupled from generic session IDs. Instead, they require a multi-phase PIN-challenge and token-issuance step: the subscriber must first authenticate (Phase 1), receiving a token that proves — for the next 120 seconds only — that this specific MSISDN completed a valid PIN check. Every subsequent action in the flow must present that token.
State Isolation
The database ledger never handles in-flight transient session state. State such as "has this user been PIN-verified in the last two minutes" is offloaded entirely to a high-availability, clustered Redis layer, keeping the relational database free to focus on its actual job: durable, ACID-compliant balance mutation. This separation also means a burst of failed or abandoned login attempts never touches the database at all.
Strict Fail-Fast Idempotency
The dynamic token is atomically validated and destroyed in a single operational step (a Redis Lua script) before any database locks are acquired. This ordering is deliberate: the cheapest possible check (an in-memory atomic compare-and-delete) is used to reject 100% of duplicate or replayed requests before the system pays the cost of opening a database transaction and taking row locks.
3.2 Why Redis, and not the relational database, for transient state
A common alternative design stores a "processed request" flag directly in the relational database (e.g. a unique constraint on a request ID column). This works, but it forces every duplicate or expired request — including malicious replay traffic — to consume a database connection and, depending on isolation level, a row or gap lock. Redis's single-threaded command execution model gives us a true atomic check-and-delete (via `GETDEL` or the Lua script in Section 6.1) at sub-millisecond latency, with the database engaged only for requests that have already been proven legitimate. This is the same pattern used by idempotency-key implementations in major payment APIs (e.g. Stripe), adapted here for a token that is single-use by construction rather than merely deduplicated.
4. High-Level Solution Architecture
4.1 Service Decomposition Philosophy: Microservices Balanced with Monolith
Financial transaction systems are a poor fit for either architectural extreme. A fully-distributed microservices design — separate services for authentication, token issuance, ledger debit, ledger credit, and audit — would require distributed transactions or a saga/compensation pattern across every one of those service boundaries, introducing exactly the kind of eventual-consistency window that this document's entire premise (Section 2) is designed to eliminate. A fully monolithic design, on the other hand, would force stateless, cheaply-scaled concerns like PIN authentication and SMS notification to scale and deploy in lockstep with the financially sensitive core, wasting compute and slowing releases.
This design therefore draws the service boundary along consistency requirements rather than along business capability alone:
4.2 Component Architecture Diagram
Our architecture resolves these vulnerabilities by utilizing a transient, single-use, application-driven Transaction Token mapped directly to the subscriber's MSISDN within an atomic memory fabric (a clustered, highly-available Redis deployment). The protocol is built on three design principles.
3.1 Design Principles
Explicit Handshake
Transactions are decoupled from generic session IDs. Instead, they require a multi-phase PIN-challenge and token-issuance step: the subscriber must first authenticate (Phase 1), receiving a token that proves — for the next 120 seconds only — that this specific MSISDN completed a valid PIN check. Every subsequent action in the flow must present that token.
State Isolation
The database ledger never handles in-flight transient session state. State such as "has this user been PIN-verified in the last two minutes" is offloaded entirely to a high-availability, clustered Redis layer, keeping the relational database free to focus on its actual job: durable, ACID-compliant balance mutation. This separation also means a burst of failed or abandoned login attempts never touches the database at all.
Strict Fail-Fast Idempotency
The dynamic token is atomically validated and destroyed in a single operational step (a Redis Lua script) before any database locks are acquired. This ordering is deliberate: the cheapest possible check (an in-memory atomic compare-and-delete) is used to reject 100% of duplicate or replayed requests before the system pays the cost of opening a database transaction and taking row locks.
3.2 Why Redis, and not the relational database, for transient state
A common alternative design stores a "processed request" flag directly in the relational database (e.g. a unique constraint on a request ID column). This works, but it forces every duplicate or expired request — including malicious replay traffic — to consume a database connection and, depending on isolation level, a row or gap lock. Redis's single-threaded command execution model gives us a true atomic check-and-delete (via `GETDEL` or the Lua script in Section 6.1) at sub-millisecond latency, with the database engaged only for requests that have already been proven legitimate. This is the same pattern used by idempotency-key implementations in major payment APIs (e.g. Stripe), adapted here for a token that is single-use by construction rather than merely deduplicated.
4. High-Level Solution Architecture
4.1 Service Decomposition Philosophy: Microservices Balanced with Monolith
Financial transaction systems are a poor fit for either architectural extreme. A fully-distributed microservices design — separate services for authentication, token issuance, ledger debit, ledger credit, and audit — would require distributed transactions or a saga/compensation pattern across every one of those service boundaries, introducing exactly the kind of eventual-consistency window that this document's entire premise (Section 2) is designed to eliminate. A fully monolithic design, on the other hand, would force stateless, cheaply-scaled concerns like PIN authentication and SMS notification to scale and deploy in lockstep with the financially sensitive core, wasting compute and slowing releases.
This design therefore draws the service boundary along consistency requirements rather than along business capability alone:
- Edge microservices (Auth Service, Notification Service) are stateless, horizontally scalable independently, and can be redeployed without touching the transactional core. A bad deploy of the notification service cannot corrupt a balance.
- The Transaction Engine is kept as a modular monolith: Token, Ledger and Audit modules run in a single deployable unit, in a single JVM, sharing one database connection pool and one local transaction boundary. This lets the debit, credit and audit-write happen inside one ACID transaction with zero network hops, which is precisely the guarantee Section 2 identifies as missing from naive designs.
4.2 Component Architecture Diagram
5. Detailed System Diagrams
Each diagram below is rendered as a figure, followed by a plain-language walkthrough.
5.1 Flow & Interaction Diagram (Dynamic Token-Exchange Sequence)
The following sequence details how the platform handles the handshake, the dynamic token validation, and the transactional database commit.
Each diagram below is rendered as a figure, followed by a plain-language walkthrough.
5.1 Flow & Interaction Diagram (Dynamic Token-Exchange Sequence)
The following sequence details how the platform handles the handshake, the dynamic token validation, and the transactional database commit.
Note that `USSDController` depends on both services through narrow interfaces, and never touches `DataSource` or `RedisClusterConnection` directly — this is what keeps the Token and Ledger modules independently testable and, if capacity ever demands it, independently extractable into their own services without touching controller code.
6. Production-Ready Implementation Blueprint
6.1 The Atomic Token-Burn Lua Script
To avoid race conditions between checking if a token is valid and deleting it, we must execute the validation and removal in a single atomic step. Redis executes Lua scripts atomically with respect to all other commands — no other client command can run in the middle of the script — so running the following script inside our Redis cluster ensures that two concurrent requests carrying the same token can never both succeed, even under heavy concurrent load.
6. Production-Ready Implementation Blueprint
6.1 The Atomic Token-Burn Lua Script
To avoid race conditions between checking if a token is valid and deleting it, we must execute the validation and removal in a single atomic step. Redis executes Lua scripts atomically with respect to all other commands — no other client command can run in the middle of the script — so running the following script inside our Redis cluster ensures that two concurrent requests carrying the same token can never both succeed, even under heavy concurrent load.
Operational tip: EVALSHA over EVAL
In production, load this script once at application startup with SCRIPT LOAD and invoke it thereafter via EVALSHA rather than EVAL. This avoids re-transmitting and re-compiling the script body on every single transfer request, which matters at the throughput targets in Section 1.4. Cache the returned SHA1 in the application and fall back to a fresh SCRIPT LOAD if Redis returns a NOSCRIPT error (this happens after a Redis restart or FLUSH).
In production, load this script once at application startup with SCRIPT LOAD and invoke it thereafter via EVALSHA rather than EVAL. This avoids re-transmitting and re-compiling the script body on every single transfer request, which matters at the throughput targets in Section 1.4. Cache the returned SHA1 in the application and fall back to a fresh SCRIPT LOAD if Redis returns a NOSCRIPT error (this happens after a Redis restart or FLUSH).
6.2 Java Core Engine Implementation
The full engine implementation — the `USSDController`/`TokenService`/`LedgerService` responsibilities collapsed into a single class for clarity — is provided as a complete, ready-to-study Java source file rather than inline here, since it runs to roughly 150 lines including the supporting record types and a mock Redis client. Download it from the file list attached to this conversation:
MobileMoneyEngine.java — demonstrates Phase 1 (PIN verification and token issuance), Phase 2 (atomic token verify-and-burn followed by an ACID database transaction), pessimistic row locking via `SELECT ... FOR UPDATE`, and the full set of helper database methods.
The key excerpt below shows the part of the design that matters most: the transfer method never opens a database connection until the token burn has already succeeded. Note also that every outcome is expressed as a `TransactionStatus` enum value rather than a raw string literal — see the callout below the excerpt for why this matters.
The full engine implementation — the `USSDController`/`TokenService`/`LedgerService` responsibilities collapsed into a single class for clarity — is provided as a complete, ready-to-study Java source file rather than inline here, since it runs to roughly 150 lines including the supporting record types and a mock Redis client. Download it from the file list attached to this conversation:
MobileMoneyEngine.java — demonstrates Phase 1 (PIN verification and token issuance), Phase 2 (atomic token verify-and-burn followed by an ACID database transaction), pessimistic row locking via `SELECT ... FOR UPDATE`, and the full set of helper database methods.
The key excerpt below shows the part of the design that matters most: the transfer method never opens a database connection until the token burn has already succeeded. Note also that every outcome is expressed as a `TransactionStatus` enum value rather than a raw string literal — see the callout below the excerpt for why this matters.
Why an enum instead of scattered string literals
The original excerpt returned raw strings such as "00_SUCCESS" and "REJECTED_INSUFFICIENT_FUNDS" directly from processTransfer(). Left as free-standing literals, these are easy to typo, impossible for the compiler to check, and force every caller to fall back to string comparison. The reference implementation instead defines a TransactionStatus enum with one constant per outcome, each carrying its own stable wire-level code() (e.g. "00_SUCCESS") and a human-readable description(). Callers switch on the enum — which the compiler can verify is exhaustive — and the wire-level code lives in exactly one place rather than being re-typed at every call site and in the API contract in Appendix A.
The original excerpt returned raw strings such as "00_SUCCESS" and "REJECTED_INSUFFICIENT_FUNDS" directly from processTransfer(). Left as free-standing literals, these are easy to typo, impossible for the compiler to check, and force every caller to fall back to string comparison. The reference implementation instead defines a TransactionStatus enum with one constant per outcome, each carrying its own stable wire-level code() (e.g. "00_SUCCESS") and a human-readable description(). Callers switch on the enum — which the compiler can verify is exhaustive — and the wire-level code lives in exactly one place rather than being re-typed at every call site and in the API contract in Appendix A.
6.3 Replacing the In-Memory Mock with a Real Redis Client
The reference implementation ships with an in-memory `RedisMockClient` so the engine can be unit-tested without external infrastructure. In production this is replaced with a real clustered client — for example Spring Data Redis backed by Lettuce — with the same atomic-burn contract implemented via `EVALSHA`:
The reference implementation ships with an in-memory `RedisMockClient` so the engine can be unit-tested without external infrastructure. In production this is replaced with a real clustered client — for example Spring Data Redis backed by Lettuce — with the same atomic-burn contract implemented via `EVALSHA`:
6.4 Idempotency & Retry Handling at the Gateway Layer
Even with the token protocol in place, the USSD gateway itself may retry an HTTP call to the Transaction Engine if it does not receive a timely response (as distinct from the subscriber retrying). To make the API safe against this class of retry as well, every mutating endpoint additionally accepts a client-supplied `Idempotency-Key` header, cached for a short window so a retried HTTP call with an identical key returns the original cached response rather than re-invoking the token-burn logic a second time:
Even with the token protocol in place, the USSD gateway itself may retry an HTTP call to the Transaction Engine if it does not receive a timely response (as distinct from the subscriber retrying). To make the API safe against this class of retry as well, every mutating endpoint additionally accepts a client-supplied `Idempotency-Key` header, cached for a short window so a retried HTTP call with an identical key returns the original cached response rather than re-invoking the token-burn logic a second time:
6.5 Resilience: Timeouts, Retries and Circuit Breaking
The Redis and database calls in the transfer path are wrapped with Resilience4j to bound latency and prevent a slow downstream dependency from exhausting the engine's request-handling threads:
The Redis and database calls in the transfer path are wrapped with Resilience4j to bound latency and prevent a slow downstream dependency from exhausting the engine's request-handling threads:
When the circuit breaker opens (database tier unhealthy), the engine fails fast with `SYSTEM_DATABASE_FAULT` rather than queuing requests behind a struggling database — which, combined with the already-burned token, means a legitimately retried request after recovery is treated as a fresh transaction rather than a blocked duplicate.
6.6 Security Hardening
A financial transaction path is only as trustworthy as the operator's ability to see what it is doing in real time. Three signal types are emitted from every request:
6.6 Security Hardening
- PIN storage: PINs are never stored or logged in plaintext. Use a slow, salted hash (BCrypt or Argon2id) with a per-record salt; a 4–6 digit PIN has low entropy, so the hash function's cost factor is the primary defence against offline brute-force.
- Encryption at rest: The relational Ledger DB — balances, PINs' hashes, and the transaction audit trail — is encrypted at rest using the database engine's native Transparent Data Encryption (TDE) or an equivalent full-disk/volume encryption layer, with keys managed by a cloud KMS or HSM rather than stored alongside the data. This is a baseline control independent of application-level hashing: it protects against a stolen disk, an unencrypted backup, or a misconfigured storage snapshot exposing balances and PII even if every other control in this section holds. Backups and read replicas inherit the same encryption policy — an unencrypted replica or backup is a bypass of the primary's protection, not a separate risk to accept.
- Transport security: All gateway-to-engine traffic runs over TLS 1.2+, ideally with mutual TLS so the engine can cryptographically verify it is talking to an approved MNO gateway, not an arbitrary caller.
- Online brute-force protection: Rate-limit PIN attempts per MSISDN (e.g. 5 attempts per 15 minutes) using the same Redis cluster, and lock the account after the threshold, requiring an out-of-band unlock.
- Replay protection beyond the token: The token itself already prevents transfer replay; additionally validate that `amountInCents` and `recipientMsisdn` in the transfer call are consistent with what was quoted to the user during the handshake screen, to prevent a tampered client from altering the payload after token issuance.
- Least-privilege database access: The Transaction Engine's database credential should have `SELECT`/`UPDATE`/`INSERT` on exactly the tables it needs and no DDL rights, limiting blast radius if application credentials are ever compromised.
A financial transaction path is only as trustworthy as the operator's ability to see what it is doing in real time. Three signal types are emitted from every request:
- Structured logs: JSON logs correlated by a request-scoped trace ID and (where present) the transaction token, with the MSISDN masked (e.g. `07•••••222`) to limit PII exposure in log aggregation systems.
- Metrics (Micrometer / Prometheus): `token.issued.count`, `token.burn.success.count`, `token.burn.rejected.count`, `ledger.commit.latency` (histogram), `ledger.rollback.count` by reason code — these four alone are enough to detect a double-spend attempt pattern or a stalled database tier before customers report it.
- Distributed tracing: A single trace spans Gateway → Auth Service → Transaction Engine → Redis → Database, so a slow transfer can be attributed to a specific hop rather than guessed at.
7. Scalability & Capacity Planning
Sizing below is derived from the non-functional targets in Section 1.4 (500 tx/s sustained, 1,000 tx/s burst) and is intended as a starting point for load testing, not a substitute for it.
7.1 Redis Cluster Sizing
Because all transactional state lives in Redis and the database — not in the JVM's memory — the Transaction Engine instances are stateless and can be scaled horizontally behind the load balancer with no session-affinity requirement. This is a direct benefit of the State Isolation principle in Section 3.1: any engine instance can service any request for any MSISDN.
Sizing below is derived from the non-functional targets in Section 1.4 (500 tx/s sustained, 1,000 tx/s burst) and is intended as a starting point for load testing, not a substitute for it.
7.1 Redis Cluster Sizing
- Each token operation (SET on auth, GETDEL on transfer) is O(1) and sub-millisecond; a single Redis node comfortably handles tens of thousands of ops/sec, so cluster sizing here is driven by availability, not raw throughput.
- Recommended topology: 6 shards × 3 replicas (1 primary + 2 replicas each) for cross-AZ resilience, with `min-replicas-to-write 1` so a primary refuses writes if it cannot confirm replication, favouring consistency over availability per the CP stance in Section 8.2.
- Token payloads are tiny (~10 bytes) with a 120-second TTL, so memory footprint scales with concurrent in-flight sessions, not historical volume — at 1,000 tx/s and a 120s window, roughly 120,000 keys are live at any instant, a trivial memory footprint for a clustered deployment.
- Pessimistic row locks (Section 8, `SELECT ... FOR UPDATE`) are held only for the duration of the debit/credit/audit write, typically single-digit milliseconds, so connection pool sizing follows Little's Law: at 500 tx/s and ~10ms average hold time, roughly 5–10 concurrent connections are needed for the happy path; provision headroom (e.g. 50) to absorb latency spikes without connection starvation.
- Horizontal read scaling (balance lookups outside the transfer path, e.g. for a "check balance" USSD menu option) is served from read replicas, keeping the primary's connection budget reserved for writes.
- If write volume eventually exceeds a single primary's capacity, the ledger table is a natural candidate for sharding by MSISDN hash, since the transfer transaction already touches at most two account rows and can be routed to the shard(s) owning those MSISDNs.
Because all transactional state lives in Redis and the database — not in the JVM's memory — the Transaction Engine instances are stateless and can be scaled horizontally behind the load balancer with no session-affinity requirement. This is a direct benefit of the State Isolation principle in Section 3.1: any engine instance can service any request for any MSISDN.
| mobilemoneyengine.java |
RSS Feed