Skip to content

Tradeoffs

Antonio Membrides Espinosa edited this page Jul 11, 2026 · 2 revisions

BIAN Compliance and Open Banking: Architecture Trade-offs

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.


Table of Contents

  1. Trade-off 1: PII separation (SD-13 party)
  2. Trade-off 2: QE collection re-creation on rename
  3. Trade-off 3: bianServiceDomain with spaces
  4. Trade-off 4: Expanded lifecycle states
  5. Trade-off 5: Two-SD auth boundary (SD-16 vs SD-91)
  6. Open Banking performance model
  7. Alternatives comparison table

1. PII Separation

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.


2. QE Collection Re-creation

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:

  1. Create new QE collections with correct names
  2. Decrypt existing documents using old QE client
  3. Re-encrypt and insert into new collections
  4. Verify counts and checksums
  5. Drop old collections

This is a one-time migration. QE collection names are stable after migration.


3. bianServiceDomain with Spaces

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 time

4. Expanded Lifecycle States

Decision: 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."


5. Two-SD Auth Boundary

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 Performance Model

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.


Alternatives Comparison Table

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.


Later platform trade-offs (within v2)

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

Clone this wiki locally