-
Notifications
You must be signed in to change notification settings - Fork 0
Tradeoffs
This document explains the performance, operational, and design consequences of implementing strict BIAN (Banking Industry Architecture Network) and preparing for Open Banking / Open Finance. It is intended for FSI solution architects, QSAs (Qualified Security Assessors), and prospects evaluating this demo as a reference architecture.
- Trade-off 1: PII separation (SD-13 party)
- Trade-off 2: QE collection re-creation on rename
- Trade-off 3: bianServiceDomain with spaces
- Trade-off 4: Expanded lifecycle states
- Trade-off 5: Two-SD auth boundary (SD-16 vs SD-91)
- Open Banking performance model
- Alternatives comparison table
Decision: PII (email, phone, name) lives in party (SD-13) only. All other SDs reference parties via partyInstanceReference (FK).
Why BIAN requires this: BIAN SD-13 (Party Data Management) is the designated owner of party identity data. Storing PII in SD-53 (Customer Agreement) would violate the Service Domain boundary and create duplicate PII stores with inconsistent update semantics.
Performance impact:
Every customer lookup by email or phone now requires two sequential QE queries instead of one:
Step 1: QE equality search on party.partyEmailAddress
→ returns partyInstanceReference (~20-40ms on Atlas M10)
Step 2: Plaintext index lookup on customerAgreementProcedure.partyInstanceReference
→ returns customerAgreement document (~5-10ms on Atlas M10)
Total additional latency: +25-50ms per email/phone lookup
For account-reference lookups (customerAgreementReference), there is no change — the account reference lives in customerAgreementProcedure and is searchable directly via QE:equality.
P95 latency impact (estimated Atlas M10):
| Endpoint | Before (one QE query) | After (two QE queries) | Change |
|---|---|---|---|
GET /customer?email= |
150-250ms | 200-350ms | +50-100ms |
GET /customer?phone= |
150-250ms | 200-350ms | +50-100ms |
GET /customer?accountRef= |
150-250ms | 150-250ms | No change |
GET /transactions?email= |
250-400ms | 350-500ms | +100ms |
GET /fraud?caseId= |
100-150ms | 100-150ms | No change |
Alternatives considered:
| Alternative | Pros | Cons | Decision |
|---|---|---|---|
Keep email in customerAgreementProcedure as read-only copy |
Zero additional latency | Violates BIAN SD-13; duplicate PII requires synchronisation | Rejected |
$lookup aggregation across party and customerAgreementProcedure
|
Single query | ADR-001: MongoDB prohibits $lookup across QE collections |
Technically blocked |
| Party lookup cache in API process (Redis / in-memory) | Eliminates second round-trip | Adds infrastructure dependency; cache invalidation complexity | Viable for v3 NFR optimisation if P95 > 500ms |
Denormalize partyInstanceReference into cardTransactionLog
|
Reduces fraud investigation lookup depth by one hop | Redundant FK; increases write fan-out on party update | Rejected |
Accepted NFR adjustment: P95 threshold for email/phone lookup endpoints increases from 300ms to 500ms. Documented in docs/roadmap.md as NFR-v3 update.
Decision: Collections were renamed to match BIAN Control Record type names. QE collections were dropped and re-created with the new names.
Why re-creation is required:
MongoDB Queryable Encryption stores the encryptedFields metadata in the collection options at creation time. This metadata is part of the collection's identity and cannot be updated via db.collection.renameCollection(). Renaming a QE collection results in the new name having no encryptedFields configuration, which silently disables QE for writes.
Rename map (7 QE collections re-created):
| Old name | New BIAN name | Data impact |
|---|---|---|
customerAgreement |
customerAgreementProcedure |
Data loss on re-creation; re-seed required |
customerAgreementSensitive |
merged inline into customerAgreementProcedure (v2) |
Separate collection eliminated; QE:none fields now inline |
cardTransaction |
cardTransactionLog |
Data loss on re-creation; re-seed required |
cardTransactionSensitive |
merged inline into cardTransactionLog (v2) |
Separate collection eliminated; QE:none fields now inline |
paymentCard |
paymentCardManagement |
Data loss on re-creation; re-seed required |
partyAuthentication |
partyAuthenticationAssessment |
Moved to plaintext (credentials to SD-91) |
customerCreditRating |
customerCreditRatingState |
Plaintext collection, renamed safely |
Consequence for demo: Acceptable. The demo is seeded from JSON files. After running npm run setup:db --reset, re-seeding restores all data.
Consequence for production: A full data migration pipeline would be required:
- Create new QE collections with correct names
- Decrypt existing documents using old QE client
- Re-encrypt and insert into new collections
- Verify counts and checksums
- Drop old collections
This is a one-time migration. QE collection names are stable after migration.
Decision: All documents carry bianServiceDomain values with spaces as BIAN specifies: 'Customer Agreement', 'Card Transaction', 'Fraud Diagnosis', etc.
Why BIAN requires this:
The BIAN Service Domain catalogue uses natural-language names with spaces. Using 'CustomerAgreement' (PascalCase) is an informal shorthand not defined in the standard. For a demo targeting FSI architects and QSAs, the field must match the catalogue exactly.
Functional impact: None. The bianServiceDomain field is metadata only; it is never used as a query predicate. MongoDB stores string values with spaces without issue.
Developer ergonomics: Slightly more verbose in TypeScript. Mitigated by TypeScript literal types:
bianServiceDomain: 'Customer Agreement';
// TypeScript compiler rejects 'CustomerAgreement' at compile timeDecision: AgreementStatus and CardStatus TypeScript union types now include all BIAN-defined states, not just the subset used in the demo.
Added states:
| Type | Added states | BIAN meaning |
|---|---|---|
AgreementStatus |
initiated, agreed, amended, dormant
|
Pre-agreement, terms accepted, terms changed, temporarily inactive |
CardStatus |
issued, suspended, revoked
|
Card created but not yet activated, temporarily suspended by bank, permanently revoked |
Impact on frontend: Any component that switches on AgreementStatus or CardStatus will need a default case to handle the new states without crashing. Existing demo data only uses active for agreements and active for cards; the new states are valid at the model level but not exercised in v1-v3.
Decision: Add all BIAN states at the model level now. The TypeScript compiler prevents invalid states from being written. The UI handles only demo-relevant states (active, suspended, closed, blocked). Document unused states as "valid BIAN states, reserved for v4 lifecycle features."
Decision: Authentication responsibilities are split between two Service Domains:
-
SD-16 Party Authentication (
partyAuthenticationAssessment): identity verification events only -
SD-91 Customer Authentication (
customerAuthenticationAssessment): login credentials, roles, and access state
Why BIAN requires this: SD-16 is an identity verification SD. It asserts that a party is who they claim to be, using formal verification methods (document scan, biometric check, OTP). It does not own login credentials or application roles. SD-91 (Customer Authentication) is the correct home for login state: bcrypt hashes, role assignment, last-login timestamp, account lock state.
Operational impact:
| Concern | Old design (SD-16 only) | New design (SD-16 + SD-91) |
|---|---|---|
| Login query | partyAuthentication.partyAuthenticationUserEmailAddress |
customerAuthenticationAssessment.customerAuthenticationEmailAddress |
| JWT sub | partyAuthenticationInstanceReference |
customerAuthenticationInstanceReference |
| Role lookup | partyAuthentication.partyAuthenticationUserRole |
customerAuthenticationAssessment.customerAuthenticationUserRole |
| Collections | 1 (partyAuthentication) | 2 (partyAuthenticationAssessment + customerAuthenticationAssessment) |
Migration risk: Existing fraud case documents referencing analystInstanceReference and investigatorInstanceReference still point to valid UUIDs. The UUIDs were preserved when migrating from partyAuthentication to customerAuthenticationAssessment (old partyAuthenticationInstanceReference = new customerAuthenticationInstanceReference). No FK repair required.
Open Banking consent-scoped access adds a third lookup step for TPP (Third-Party Provider) requests:
Step 1: Validate the OAuth 2.0 / OIDC access token (now implemented)
→ verify consent is active and not expired (~5ms, in-process)
Step 2: QE equality search on party or customerAgreementProcedure
→ resolve the customer's data scope (~20-40ms)
Step 3: Apply consent scope filter (which fields the TPP is allowed to see)
→ in-process, zero additional DB round-trip (~1ms)
Step 4: Return filtered response
Consent revocation latency: Revoking a consent grant sets consentStatus: 'revoked' in consentAgreement. Because step 1 checks this field on every request, revocation takes effect on the next API call. There is no token TTL lag. This is a stronger guarantee than OAuth 2.0 token expiry alone.
Future DEK-per-consent scope: A DEK-consent-<scope> can be issued per customer consent grant (forward-looking). Revoking a consent grant means revoking the DEK in AWS KMS: the TPP can no longer decrypt previously authorized fields, even from cached Atlas ciphertext. This is the cryptographic revocation guarantee referenced in the Open Finance readiness section of architecture.md.
| Concern | Option A (strict BIAN) | Option B (pragmatic) | Chosen |
|---|---|---|---|
| PII placement |
party (SD-13) only |
email/phone in customerAgreementProcedure
|
A |
| Auth credentials |
customerAuthenticationAssessment (SD-91) |
partyAuthentication (SD-16) |
A |
| Collection naming | CR type suffix: customerAgreementProcedure
|
Short name: customerAgreement
|
A |
| bianServiceDomain |
'Customer Agreement' (with spaces) |
'CustomerAgreement' (PascalCase) |
A |
| Consent management |
consentAgreement + consentAccessLog (SD-36 stub) |
No consent layer in v1-v3 | A |
| FK naming in fraud cases | cardTransactionInstanceReference |
linkedCardTransactionReference |
A |
| Lifecycle states | Full BIAN set (issued, agreed, dormant, etc.) |
Demo-subset only (active, suspended, closed) |
A |
All seven decisions favour strict BIAN compliance. The trade-off accepted in all cases is that the code is slightly more verbose and the two-step PII lookup adds 25-50ms latency to email/phone searches. For a compliance demo targeting QSAs and FSI architects, correctness outweighs convenience.
As the platform matured within v2 (Integration Hub, event audit, bank transfers, merchant unification, passwordless), a few more architectural trade-offs were taken:
| Decision | Trade-off | Why |
|---|---|---|
| Internal-first provider dispatch (Integration Hub, SD-193) | An extra indirection (dispatchProvider) on every capability call |
Provider indifference: a built-in Module answers on a sub-5ms hot path, and an external vendor can substitute it with no caller change |
Timeseries event audit (businessProcessEvent / complianceProcessEvent, TTL 90/365 days) |
A separate audit write per business/compliance event; fire-and-forget | Append-only, queryable, bounded-growth audit that satisfies PCI Req 10 without bloating domain documents; CHD is blocklisted from event payloads |
| Data-driven ACL (ADR-030) | Permissions resolved per request from the role collection, not baked into the JWT |
Roles/permissions change without re-issuing tokens; least-privilege is enforced server-side and cannot be self-elevated |
| Dual-auth merchant unification (ADR-042) | One middleware must accept two token types (session HS256 or merchant OAuth RS256) | Merchant = OAuth client on the same shared modules; no forked /merchant/* surface to keep in sync |
| PSP holds no balances (bank transfers) | Every transfer is external (PISP); no internal ledger of record | Keeps the demo out of money-transmitter / core-banking scope while still exercising ACH/SEPA/SWIFT rails and mandates |
| Passwordless: public keys only (CIBA / WebAuthn) | Server stores only public keys; the private key is a non-extractable browser credential | Even a full DB compromise cannot impersonate a user; aligns with FIDO2 and PCI Req 8 |