Skip to content

Architecture Decision Records (ADR)

About This Page

What: Key design decisions in the deposit/withdrawal system — their background, alternatives considered, final choice, and consequences — to help understand "why it is this way" Audience: Product managers assessing the impact of requirement changes; newcomers understanding system design logic Prerequisites: System Architecture & Data FlowFormat: Each decision follows the "Background → Alternatives → Decision → Consequences" structure Reading time: 4 minutes Owner: Deposit & Withdrawal Product Team

Key takeaway: Each architecture Decision record documents "background → alternatives → final choice → consequences." When PMs assess requirement changes, first check the relevant ADR to understand "why it is this way" and avoid repeating past mistakes.


ADR-001: Deposit Matching Uses a Five-Dimension Matching Engine

Status: Adopted · In effect

Background: When users deposit funds to their Securities Account via bank transfer, the system needs to pair the Bank Statement (Flow) with the user's deposit application (Apply). The early approach was purely manual Matching, but as user volume grew, manual processing could not scale.

Alternatives:

OptionProsCons
A. Purely manual MatchingHighest accuracyNot scalable, poor processing timeliness
B. Single-dimension Matching (amount only)SimpleHigh false-match rate (same amount, different users)
C. Five-dimension Matching (amount + currency + bank + date + card number)High accuracy, automatableComplex implementation, requires per-bank tolerance maintenance
D. Bank returns unique IDMost preciseMost banks do not support this (only BST can do it)

Decision: Adopt Option C — five-dimension Matching engine with a two-tier tolerance mechanism (auto-credit tolerance + assisted Matching tolerance).

Consequences:

  • Positive: ~95% of deposits can be automatically matched and credited; Operations only handles ~5% exceptions
  • Negative: Requires maintaining separate Matching rules and tolerance parameters for each bank (12 banks, 12 rule sets)
  • Change impact: When adding a new bank, a corresponding Match class must be implemented (e.g., BocMatch.php, HsbcMatch.php) and tolerance parameters configured

Code location: deposit/src/app/Business/Match/ directory, per-bank Match classes


ADR-002: Withdrawal Uses a Three-Step Approval Workflow (Audit → Confirm → Remittance)

Status: Adopted · In effect

Background: Withdrawals involve real funds leaving the system and require strict Approval controls. However, overly cumbersome Approval processes hurt user experience. A balance between security and efficiency is needed.

Alternatives:

OptionProsCons
A. Single-step Approval (direct remittance)FastestHigh security risk
B. Two-step Approval (review + remittance)SimpleChannel selection cannot be independently reviewed
C. Three-step Approval + 6 templatesFlexible; normal withdrawals skip AuditComplex implementation
D. Fully manual workflowMost securePoor timeliness, not automatable

Decision: Adopt Option C — three-step Approval (Audit → Confirm → Remittance), with 6 templates controlling which withdrawals require which steps. Normal withdrawals (default template) skip Audit and go directly to Confirm.

Consequences:

  • Positive: Normal withdrawals need only 2 steps; high-risk withdrawals get an extra review layer
  • Positive: BST Channel can complete fully automatically when 6 conditions are met, with no manual intervention
  • Negative: Maintenance cost of 6 templates; adding a new Approval step requires creating a Step class
  • Change impact: Adding an Approval step → create a Step class implementing the IFStep interface → update $stepTemplates

Code location:

  • Template definitions: withdraw/src/app/Business/Task.php$stepTemplates
  • Step implementations: withdraw/src/app/Business/Tasks/Step/{Audit,Confirm,Remittance}.php

ADR-003: BST Uses a "Unified Mainland Bank Integration + Bank Adapter Layer" Architecture

Status: Adopted · In effect

Background: CMB, Minsheng, and Airstar all support BST (Bank-Securities Transfer), but their integration protocols are completely different: CMB/Minsheng use SM2-encrypted Socket, while Airstar uses HTTPS REST API + Mandate model.

Alternatives:

OptionProsCons
A. Independent system per bankNo mutual impactRedundant development, difficult unified scheduling
B. Unified business layer + bank adapter layerShared business logic, bank differences encapsulatedComplex adapter layer design
C. Unified protocol conversion gatewaySimplestProtocol differences too large; forcing unification loses bank-specific features

Decision: Adopt Option B. Deposit/withdrawal business flows (application, Approval, crediting) are unified at the upper layer, while bank communication differences are encapsulated in independent services (cmb_stock_trans, ms_stock_bank_transaction, Airstar API layer).

Consequences:

  • Positive: Adding a new BST bank only requires implementing the adapter layer without modifying business logic
  • Negative: Airstar's async polling model differs significantly from CMB/Minsheng's real-time push model; the upper layer must accommodate both modes
  • Change impact: Airstar withdrawals require additional polling tasks (AsbBstTransfer + SyncAsbBstWithdraw), while CMB/Minsheng do not

ADR-004: eDDA/eDDI Concurrency Control Uses Token Bucket (HSBC) + Pessimistic Locking (Hang Seng)

Status: Adopted · In effect

Background: Both HSBC and Hang Seng eDDI debit interfaces have concurrency limits. If concurrent requests are too numerous, the bank will reject or delay processing. Rate control on the system side is required.

Alternatives:

OptionProsCons
A. Global mutexSimplestExtremely low throughput
B. HSBC: Token Bucket optimistic lockingHigh throughput, smooth rate limitingMore complex implementation
C. Hang Seng: InnoDB row-level lock serializationStrictly ordered, satisfies trading session constraintsLimited throughput
D. Message queue rate limitingDecoupledIntroduces additional infrastructure

Decision:

  • HSBC: Token Bucket optimistic locking (UPDATE ... WHERE tokens >= 1), 4 tokens per second, persisted in database
  • Hang Seng: InnoDB row-level pessimistic locking (SELECT ... FOR UPDATE), because Hang Seng has trading session requirements that demand strict serialization

The core reason for using different approaches: Hang Seng needs to be aware of trading sessions (requests outside trading hours enter new_blank status), requiring strict request ordering; HSBC has no such constraint, so Token Bucket provides higher throughput.

Consequences:

  • Positive: Both approaches are tailored to their respective bank's characteristics and run stably
  • Negative: Two concurrency control mechanisms increase maintenance cost
  • Change impact: Adjusting HSBC rate limiting → modify Token Bucket production rate; adjusting Hang Seng → modify row lock granularity

Code location:

  • HSBC: sba_hsbc_eddi/ Token Bucket implementation
  • Hang Seng: sba_hase_eddi/ row lock implementation

ADR-005: Withdrawal Channel Selection Uses Rule-Based Routing + Manual Fallback

Status: Adopted · In effect

Background: The system supports 12 withdrawal Channels (Methods) and needs to automatically select the optimal Channel based on the user's Bank Card, currency, amount, and other conditions. However, some scenarios cannot be automatically determined.

Alternatives:

OptionProsCons
A. Fully automatic routingNo manual interventionCannot cover all edge cases
B. Automatic routing + method=null manual fallbackHigh coverage, manual safety net for exceptionsOperations staff need Channel knowledge
C. Fully manual selectionMost flexibleNot scalable

Decision: Adopt Option B. calcMethod() routes automatically based on Bank Card type; when undetermined, it sets method = null, and Operations manually selects in the Confirm step.

Consequences:

  • Positive: ~80% of withdrawal Channels can be automatically determined
  • Negative: method = null is the most common reason the Confirm step gets stuck
  • Change impact: When adding a new Bank Card type, calcMethod() routing rules must be updated, otherwise method=null will occur

Code location: withdraw/src/app/Business/CreatorBase.phpcalcMethod()


ADR-006: Bank Statement Collection Uses a "One Bank, One Service" Architecture

Status: Adopted · In effect

Background: Different banks have vastly different Bank Statement integration protocols: BOCHK uses B2E XML, HSBC uses MT910 SWIFT, ICBC uses Bank-Enterprise Direct Link, and EWB uses CSV file import.

Alternatives:

OptionProsCons
A. Unified Bank Statement collection serviceCentralized managementProtocol differences too large; forced unification is impractical
B. One independent service per bankEach service deploys and maintains independently with no mutual impactMany services
C. Group by protocol typeCompromiseGrouping criteria are unstable

Decision: Adopt Option B. Each bank has an independent Bank Statement collection service:

  • BOCHK: bochk_flow_go (Go)
  • HSBC: hsbc_bank_flow_service (Python)
  • ICBC: icbc_be_relay (Python)
  • CGB: cgb_fps_service (Go)
  • SCB: scb_service (Go)

All services write Bank Statements uniformly into acct_trd_record / {bank}_bank_flow tables, consumed by the Matching engine.

Consequences:

  • Positive: A service failure at one bank does not affect others; each service can use the most suitable tech stack
  • Negative: Many services (5+), high monitoring and ops cost
  • Change impact: Adding a bank → create an independent service + implement a Match class + register with the Matching engine

ADR-007: Deposit Reversal Simultaneously Unbinds the Bank Card

Status: Adopted · In effect (controversial)

Background: After a deposit Reversal (funds returned), should the Bank Card bound to that deposit be automatically unbound?

Alternatives:

OptionProsCons
A. Auto-unbind on ReversalPrevents the same card from causing problems againUser may need to deposit again with the same card
B. Retain binding after ReversalBetter user experienceRisky card may continue to be used
C. Decide based on Reversal reasonFine-grained controlComplex implementation

Decision: Adopt Option A — always auto-unbind on Reversal.

Consequences:

  • Positive: Reduces the risk of a problematic Bank Card repeatedly depositing
  • Negative: After chargeback-type Reversals, users need to re-bind the card to deposit with the same card again
  • Change impact: To change to "no unbind," remove the unbinding logic in deposit/src/app/Business/Reverse.php

How to Use ADRs

When product managers assess requirement changes:

  1. First check the relevant ADR to understand the background and constraints of the existing design
  2. Evaluate whether the new requirement conflicts with existing ADRs
  3. If an existing Decision needs to be overturned, create a new ADR to record the new Decision and mark the old ADR as "Deprecated"

New ADR template:

ADR Template
markdown
## ADR-XXX: Decision Title

**Status**: Proposed / Adopted / Deprecated / Superseded (by ADR-YYY)

**Background**:
Why was this decision needed? What problem was being faced at the time?

**Alternatives**:

| Option | Pros | Cons |
|------|------|------|

**Decision**: Which option was chosen, and the core reasoning.

**Consequences**:
- Positive impact
- Negative impact
- Change impact (what to watch for in future requirement changes)

**Code location**: Core code files involved

After Reading

I want to...Go to
Understand overall system architectureSystem Architecture & Data Flow
Dive into the SBA orchestration layerSBA Fund Orchestration
Understand the full deposit landscapeDeposit Methods Overview
Understand the withdrawal Approval workflowWithdrawal Lifecycle
Was this page helpful?

内部业务文档 · 仅限 moomoo 团队使用