Skip to content

About

Payment authorization switch in Core Java 17 with no frameworks: custom ISO 8583 codec, bounded thread pool, idempotent duplicate handling, circuit breaker, and a JavaScript demo

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AuthSwitch

Live demo: authswitch-web.vercel.app (free hosting, so the first load can take up to a minute to wake the API)

A small, dependency-free payment authorization switch in plain Java 17: it accepts ISO 8583 card authorization requests, routes them to the right issuer by BIN, and keeps answering when an issuer is slow or down, using stand-in processing (STIP) with later advice replay.

It models the part of the card network that sits between a merchant's acquirer and a cardholder's bank: every authorization must get an answer within a tight time budget, exactly once, even while one issuer is having an outage. Core Java, concurrency, and classic design patterns only: no frameworks, no libraries.

make test    # 16 tests, no JUnit needed
make run     # 5-phase console demo: load, outage, retransmission storm, recovery, bad input
make serve   # HTTP API on :8080 for the web control room in web/

Requires JDK 17+.

Live demo notes

authswitch-web.vercel.app: take a bank down, send traffic, and watch the circuit breaker trip, stand-in approve within limits, and advices replay when the bank recovers. The API behind it is on Render: /health.

  • Cold starts: the API runs on Render's free tier and sleeps when idle, so the first load can take up to a minute. The page shows a "can't reach the server yet" message while it wakes.
  • Shared and simulated: everyone who opens the page uses the same switch, and all banks and cards are simulated. Use Reset banks if someone left a bank down. State lives in memory (there is no database), so it resets whenever the instance restarts.
  • Right after waking, a few requests to a healthy bank can exceed the 150 ms issuer timeout and be answered by stand-in. That is the fallback working as designed, not a fault.

Live control room

web/index.html is a static page (no build step) that drives the switch through the HTTP API in demo/ApiServer.java: take a bank down or slow, send 100 transactions, and watch the circuit breaker, stand-in approvals and advice replay happen; or send one message 64 times at once and see the issuer called once.

Run it locally: make serve, then open web/index.html in a browser (it defaults to http://localhost:8080).

Deploy your own copy:

  1. Backend on Render (or Railway/Fly.io): new Web Service from this repo, runtime Docker (uses the root Dockerfile; the platform sets PORT), health check path /health. The switch is stateful (breakers, advice queues), so it needs a long-running process, not serverless. No database or other services are needed.
  2. Frontend on Vercel: import the repo, set Root Directory to web, framework preset Other, and leave the build settings empty.
  3. In web/index.html, set DEFAULT_API to your Render URL (no trailing slash) and redeploy. Visitors can also paste an API URL into the page's footer field.

The API is a simulation: it takes no real card data and has open CORS so the static page can call it.

The problem it solves

Real-world issue How the switch handles it
An issuer host is slow or down Hard per-call timeout, per-issuer circuit breaker, then stand-in approval within limits
One bad issuer must not hurt others A breaker per issuer; healthy issuers keep running (see demo phase 2)
Acquirers retransmit on timeout Idempotency: concurrent duplicates share one in-flight result; issuer is called once
Traffic spikes Bounded worker pool and queue; overflow is shed to stand-in rather than growing memory
Stand-in approvals must reach the issuer Queued as advices and replayed when the breaker closes
Card data is sensitive PAN masked everywhere (510000******0255); toString never prints field values

Architecture

 wire (0100) ─► Iso8583Codec ─► ValidationHandler chain ─► idempotency store ─► BinRouter
                  (format 30)    processing code → Luhn          (dup? share        │
                                 → amount → expiry               in-flight answer)  ▼
                                 (12/14/13/54)                           per-issuer CircuitBreaker
                                                                      closed │              │ open / half-open busy
                                                                             ▼              ▼
                                            bounded worker pool ──► Issuer (timeout)   StandInProcessor ──► advice queue
                                                    │ ok / decline       │ fail/timeout/rejected  ▲               │
                                                    └────────────────────┴────────────────────────┘      replay on CLOSED
                                                                       ▼
                                          0110 response (DE38 auth id, DE39 code) ─► observers (metrics, audit)

Patterns and concurrency, and where to find them

Technique Where Why
Builder Iso8583Message.Builder, AuthorizationSwitch.Builder Immutable messages; readable switch configuration
Chain of Responsibility validation/ValidationHandler, Validators Add or reorder rules without touching the switch
Strategy issuer/Issuer (SimulatedIssuer) Swap issuer implementations; tests use simulations
Observer observer/TransactionListener, MetricsCollector, AuditLogger Metrics/audit decoupled from the hot path; a faulty listener cannot change a result
State machine resilience/CircuitBreaker CLOSED → OPEN → HALF_OPEN with a single probe
ThreadPoolExecutor + bounded queue + AbortPolicy AuthorizationSwitch Explicit back-pressure
CompletableFuture + orTimeout AuthorizationSwitch.dispatch Non-blocking API, timeout, and worker-thread cancellation on timeout
ConcurrentHashMap.putIfAbsent idempotency store Atomic "first caller does the work, others share the future"
ConcurrentHashMap.compute StandInProcessor Atomic check-and-add keeps the cumulative cap exact
CAS loop on AtomicLong SimulatedIssuer.authorize Balance can never be overspent
LongAdder / LongAccumulator MetricsCollector Low-contention counters
Injectable clock CircuitBreaker, Validators State transitions tested without sleeping

Sample run (make run)

== Phase 1: healthy traffic, 2000 authorizations across 16 client threads
  transactions=2000  avgLatency=4.78ms  maxLatency=71.25ms
  code.00 APPROVED 1899   code.51 INSUFFICIENT_FUNDS 101   path.ISSUER 2000

== Phase 2: BANK_A goes down. BANK_B must be unaffected
  path.ISSUER 300 (BANK_B)   path.STAND_IN 300 (BANK_A)
  BANK_A breaker=OPEN   BANK_B breaker=CLOSED   pending advices for BANK_A=194

== Phase 3: retransmission storm, the same message sent by 64 threads at once
  64 identical requests -> issuer called 1 time(s), 63 duplicates suppressed, identical answers: true

== Phase 4: BANK_A recovers; a half-open probe closes the breaker and advices are replayed
  BANK_A breaker=CLOSED   advices queued=194   replayed to issuer=194   still pending=0

Project layout

src/main/java/com/authswitch/
  iso/          Iso8583Message, Iso8583Codec (bitmap, fixed + LLVAR fields, strict validation)
  model/        AuthRequest, ResponseCode (DE39), IssuerDecision, PanUtil (Luhn, masking)
  validation/   ValidationHandler (chain) + Validators
  routing/      BinRouter (TreeMap range lookup), Route
  issuer/       Issuer interface, SimulatedIssuer (latency + failure injection)
  resilience/   CircuitBreaker
  standin/      StandInProcessor (limits, advices, replay)
  observer/     TransactionListener, MetricsCollector, AuditLogger
  core/         AuthorizationSwitch (orchestration)
  demo/         Demo (console scenario), ApiServer (HTTP API on the JDK's built-in server)
web/            index.html, the static control-room page (deploy to Vercel)
Dockerfile      multi-stage build for the API (deploy to Render, Railway, Fly.io)
src/test/java/com/authswitch/AllTests.java

Design decisions worth discussing

  • A decline is a healthy answer. Issuer response 51 calls recordSuccess(); only timeouts, exceptions and pool rejection count against the breaker.
  • The switch always answers. The returned future carries a 0110 even for infrastructure failures; the stand-in path decides approve (00) or decline (91).
  • Rejected submissions release the half-open probe. If the pool rejects the probe request, the breaker is told it failed; otherwise it would wait forever for a probe result.
  • Idempotency key = terminal + STAN + RRN + transmission time, the same fields acquirers keep stable on retry. Completed entries are purged after a TTL by a housekeeping thread.
  • Timeouts cancel the worker (Future.cancel(true)) so a hung issuer call does not pin a pool thread.

Deliberately simplified

This is a learning/portfolio project, not a certified implementation.

  • ASCII ISO 8583 subset (11 data elements, primary bitmap only), not Mastercard's binary/EBCDIC formats, and no MAC, PIN, EMV or 3-D Secure fields.
  • The issuer is simulated in memory; no network I/O, TLS, or HSMs.
  • Idempotency state and advice queues are in memory, so they would not survive a restart or span nodes. A production version would persist both (and replay advices in order).
  • Advices are replayed when an issuer's circuit breaker goes back to closed. If a few requests fall back to stand-in without the breaker ever opening (for example a one-off timeout), those advices wait in the queue until a later outage and recovery. A production version would also drain the queue whenever a call succeeds.
  • Stand-in limits are static; real STIP parameters come from the issuer, by BIN and merchant category.
  • Tests use a small built-in runner instead of JUnit so the project builds with only a JDK. Porting AllTests to JUnit 5 would be mechanical.

Possible extensions

Reversals (0400) and settlement files, a socket front end using NIO (AsynchronousServerSocketChannel), a pom.xml/Gradle build with JUnit 5, JMH benchmarks for the router and codec.

About

Payment authorization switch in Core Java 17 with no frameworks: custom ISO 8583 codec, bounded thread pool, idempotent duplicate handling, circuit breaker, and a JavaScript demo

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages