Skip to content

ADR-044: External provider stub strategy for dev and UAT environments

Status Accepted
Date 2026-04-27
Deciders CTO, Head of Platform, Head of Engineering
Affects repos bank-platform, bank-core, bank-kyc, bank-aml, bank-payments, bank-credit, bank-risk-platform, bank-app

Status: Accepted — 2026-04-27

Context

Modules across all eight system domains call external third-party APIs: identity verification providers (DVS, DIA, Onfido, Equifax, Centrix), payment clearing networks (NPP, BECS, SWIFT, BPAY, ESAS, NZ faster payments), credit bureaus, and open banking connectors (Akahu, CDR). These providers cannot be used in dev and UAT:

  • They charge per call and have rate limits incompatible with CI
  • They have production-only API credentials with regulatory and contractual obligations
  • They do not support arbitrary test scenarios (fabricated identities, test sanctions hits)
  • Async networks (NPP, BECS settlement, SWIFT) have clearing windows measured in hours or days — incompatible with automated test assertions

Integration tests must be deterministic: the same test input must always produce the same outcome, regardless of when or where the test runs.

Amazon Pinpoint (the notification service, ADR-032) is a genuine AWS service available in all environments. However, integration tests need to assert that a notification was dispatched — a capability Pinpoint does not expose natively.

The XGBoost fraud model (MOD-023) is an internal ML artefact, not an external API. It requires a trained model file at deploy time, which cannot be a production model in dev/UAT.


Decision

1. Lambda stub functions behind API Gateway, with DynamoDB state.

A dedicated stub service (MOD-157) deploys one API Gateway and a set of Lambda handler functions, one per external provider category. Stubs are deployed to dev and UAT stages only; the prod stage has no stub stack.

2. SSM-based endpoint routing — no code changes between environments.

Every consuming module reads its provider endpoint URL from SSM Parameter Store. The SSM path is the same in every stage; only the value differs:

/bank-kyc/{stage}/eidv/onfido/base-url
  dev  → https://stubs-{stage}.bank-dev.internal/oidv
  uat  → https://stubs-{stage}.bank-uat.internal/oidv
  prod → https://api.onfido.com/v3

No module code changes between environments. Consuming modules are provisioned by SST and read from SSM at cold-start; the stub service writes the stub URL to the correct SSM path during its own deployment.

3. Test pattern convention: input-driven outcomes, not configuration.

Stub behaviour is driven by recognisable patterns in the request input rather than by configuration switches. Examples:

  • eIDV: document reference PASS-* → verified; FAIL-* → rejected; REFER-* → manual review; SANCTION-* → match
  • NPP clearing: destination account 000-000 / 0001 → clears; 0002 → dishonours; 0003 → timeout
  • BECS: payer BSB 062-000 → all presentments honour; 062-001 → second presentment dishonours

This makes integration tests self-contained: they pass known test inputs and assert known outputs, without needing to reconfigure the stub or depend on external test-data management.

4. Async stubs simulate the full lifecycle.

For async providers (Onfido webhook, NPP clearing, SWIFT ACK), the stub Lambda accepts the initial request, stores pending state in DynamoDB, and fires a webhook callback to the consuming module's webhook endpoint after a short delay (2–10 seconds, configurable per provider). This exercises the full two-leg flow end-to-end.

5. Notification capture via DynamoDB log.

Pinpoint is used as a real AWS service in all environments. A notification capture Lambda, subscribed to the SNS topic that MOD-063 publishes to, writes every dispatched notification (type, recipient, content, timestamp) to a DynamoDB table. Integration tests query this table to assert that a specific notification was sent to a specific customer. The capture Lambda is deployed by MOD-157.

6. Fraud model stub for MOD-023.

A rules-based stub model artefact is deployed to the model S3 path configured via SSM in dev and UAT. The stub scores based on simple rules: amounts above NZD/AUD 10,000 → 0.9 (high risk); reference containing FRAUD-TEST → 0.9; otherwise → 0.1. MOD-023 code and configuration are unchanged; only the model artefact at the SSM-configured path differs.


Alternatives considered

Option A — Per-module mocking in test code (e.g. vi.mock, nock). Rejected for integration tests. Unit tests already mock at this layer. Integration tests must exercise the real module code including its HTTP client and SSM reads — mocking inside the test defeats the purpose of the integration test.

Option B — Shared third-party sandbox accounts (e.g. Onfido sandbox). Rejected. Sandbox accounts have rate limits, cost money, require onboarding agreements, and return non-deterministic responses. They also require real-looking test identities per jurisdiction, which complicates CI. The stub approach gives full control over response scenarios.

Option C — Environment variables to switch provider. Rejected. Environment variables in Lambda functions are visible in the AWS console and leak provider configuration into the container. SSM Parameter Store is already the established pattern (ADR-030); using it for endpoint URLs is consistent.


Consequences

  • MOD-157 (provider stub service) must be deployed to a stage before any module with an external dependency can run integration tests against that stage.
  • Every consuming module's docs/design/MOD-NNN.md must document the SSM path(s) it reads for provider endpoints. The reusable-iac.yml workflow verifies SSM outputs at deploy time; the same convention extends to provider endpoint SSM paths.
  • When a new external provider is added to any module, a corresponding stub handler must be added to MOD-157 before the module's integration tests will pass.
  • Stub deployment is gated by the same DT-007 pipeline gate as all other modules — stubs cannot be manually deployed, only via CI.

All ADRs Compiled 2026-05-22 from source/entities/adrs/ADR-044.yaml