Skip to content

eddie605134/MicroBank

Repository files navigation

MicroBank — Angular Micro-Frontend Banking Workbench

A net-banking workbench that demonstrates enterprise-grade micro-frontend architecture: one shell dynamically loads three independently developed, independently deployed remotes at runtime via Native Federation v4, sharing a design system and authentication state, backed by a Fastify BFF with a simulated core-banking data layer.

Simulated data disclaimer — Everything in this project is fake: customers, accounts, balances, transactions, OTP codes, and JWT secrets are all generated from a fixed seed for demo purposes. No real financial information or real banking API is involved, and none of the code should be used against real banking systems.

Architecture

flowchart LR
    subgraph Browser
        Shell["shell (host)\n:4200 / :8090\nlogin · nav · guards"]
        A["mfe-accounts\n:4201 / :8081"]
        T["mfe-transfers\n:4202 / :8082"]
        AD["mfe-admin\n:4203 / :8083"]
    end

    M["federation.manifest.json\n(fetched at RUNTIME)"]
    BFF["Fastify BFF :3001\nJWT auth · zod validation\nin-memory core banking (seeded)"]
    SH["@microbank/shared (singleton)\nzod contracts · auth store · event bus · api client"]
    UI["@microbank/ui\nmb-* design system"]

    Shell -- "1. fetch manifest" --> M
    Shell -- "2. loadRemoteModule('./routes')" --> A
    Shell -- "2. loadRemoteModule('./routes')" --> T
    Shell -- "2. loadRemoteModule('./routes')" --> AD
    A & T & AD & Shell -- "REST + Bearer JWT" --> BFF
    A & T & AD & Shell -. "federated singleton" .-> SH
    A & T & AD & Shell -. "shared components" .-> UI
Loading

Runtime loading flow: the shell's main.ts calls initFederation('federation.manifest.json'), which fetches the manifest at runtime (never baked in at build time), resolves each remote's remoteEntry.json, wires up import maps (es-module-shims), and only then boots Angular. Each route like /transfers lazily calls loadRemoteModule('mfe-transfers', './routes'); if that fails, the route degrades to an error card with retry — the shell and every other remote keep working.

Why micro-frontends (and when not to)

I spent years building net-banking systems for five different banks in Taiwan. The recurring organizational reality: a "bank portal" is never one team's product. Accounts, transfers, wealth management, and the staff back-office are owned by different teams with different release cadences, different risk appetites (a transfer flow change needs far heavier sign-off than a dashboard tweak), and often different vendors. A single deployable front-end turns every release into a cross-team negotiation, and one team's regression blocks everyone's release train.

Micro-frontends map deployment boundaries onto those team boundaries: the transfers team ships when transfers is ready, and a broken admin module degrades to an error card instead of taking the whole portal down (both demonstrated below).

The honest trade-offs — this architecture is expensive:

  • Version alignment across shell and remotes becomes an operational discipline, not a package.json detail (see Shared dependency strategy).
  • Local development needs five processes (or containers) instead of one.
  • Cross-remote communication, shared auth, and design consistency all need explicit contracts that a monolith gets for free.
  • Debugging spans multiple origins and multiple builds.

When you should not use micro-frontends: a single team, a product where everything releases together anyway, or an app small enough that lazy-loaded Angular routes already give you the modularity you need. In those cases a well-structured monolith with lazy routes delivers the same UX with a fraction of the complexity. Use micro-frontends when organizational scaling is the bottleneck — not for technical novelty.

Native Federation v4 vs Webpack Module Federation

Native Federation v4 Webpack Module Federation
Underlying build esbuild (Angular CLI default builder) webpack (retired from Angular CLI)
Mechanism Import maps + es-module-shims, remoteEntry.json metadata webpack runtime containers, remoteEntry.js
Angular alignment Versioned with Angular (NF 22.x ↔ Angular 22.x), maintained by Angular Architects Requires keeping webpack in the toolchain Angular has moved away from
Standards Browser-native ES modules webpack-specific runtime

This project uses @angular-architects/native-federation v4 (the rewrite that moved to the native-federation/angular-adapter repository, split core into @softarc/native-federation 4.x plus a new orchestrator). Choosing it keeps the stock Angular esbuild pipeline, follows Angular's own major-version cadence, and builds on web standards (import maps) rather than a bundler-specific runtime. Module Federation remains the right call for webpack-locked ecosystems or when you need its mature dynamic-remote tooling — but for a green-field Angular 22 system, Native Federation is the aligned choice.

Shared dependency strategy

Configured in each app's federation.config.mjs via shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }):

Package Singleton? Why
@angular/core / common / router Must Two Angular runtimes = two DI containers, broken inject() contexts (NG0203), double change detection
rxjs Must Cross-instance instanceof/Symbol mismatches break interop
@microbank/shared Must Holds the auth store and event bus as module-level signals — a second copy silently splits auth state and drops cross-remote events
@microbank/ui Shared (stateless) Correctness-wise it could be duplicated; sharing it just avoids shipping the design system four times

Version skew policy: strictVersion: true makes a mismatch a loud runtime error instead of a subtle state-splitting bug. All apps pin the same Angular minor in one pnpm workspace, so skew can't creep in locally; in a real multi-repo setup you would enforce this with a platform BOM (a versions manifest every team consumes) and contract tests in CI. If a remote must ship a newer Angular before the shell, that is a coordinated platform upgrade, not something requiredVersion can paper over.

Cross-remote communication

When a transfer completes in mfe-transfers, mfe-accounts must refresh balances — without a reload. The event bus lives in packages/shared/src/events/event-bus.ts:

  • Because @microbank/shared is a federated singleton, a plain module-level Angular signal is already the same object instance in every remote.
  • emitEvent('transfer:completed', payload) in mfe-transfers; mfe-accounts reacts with an effect() — fully typed, and integrates directly with zoneless/OnPush change detection.

Alternatives considered:

Approach Verdict
Signals in a shared singleton (chosen) Typed, zero-boilerplate, plays natively with Angular reactivity. Constraint: only works between remotes that share the module — true here, since all remotes are Angular
CustomEvent on window Framework-agnostic (right call for polyglot remotes), but stringly-typed payloads and manual change-detection wiring
Shared RxJS Subject Works, but adds subscription lifecycle management that signals make unnecessary
URL/query params Great for navigational state, wrong for "data changed" notifications

Monorepo layout

apps/
  shell/           # host: login, nav, layout, guards, runtime remote loading (4200)
  mfe-accounts/    # remote: accounts overview, transactions, detail drawer (4201)
  mfe-transfers/   # remote: 5-step transfer wizard + payee CRUD (4202)
  mfe-admin/       # remote: staff back-office, lock/unlock + audit log (4203)
  bff/             # Fastify BFF, JWT auth, seeded in-memory core banking (3001)
packages/
  ui/              # mb-* design system (standalone + signals + OnPush, Tailwind v4 tokens)
  shared/          # zod contracts (single source), auth store, event bus, api client
deploy/            # nginx configs + docker runtime manifest
docs/DEVLOG.md     # per-stage engineering log (decisions & trade-offs)

Tech: Angular 22 (zoneless, signals, new control flow) · Native Federation 22 (v4) · Fastify 5 · zod 4 · Tailwind CSS 4 · pnpm workspaces · Vitest. TypeScript strict everywhere.

Quick start

Requires Node ≥ 24 (nvm use) and pnpm 10, plus Docker for the four-origin demo. Zero external dependencies — the BFF seeds an in-memory core bank (fixed seed, fully reproducible).

pnpm install
pnpm dev        # BFF :3001 + four dev servers :4200-:4203

Open http://localhost:4200 and log in (simulated credentials, OTP is always 000000):

User Password Role
amy demo1234 customer
david demo1234 customer
staff staff1234 staff (sees /admin)

JWTs live in memory only (no localStorage): storage-based tokens are readable by any XSS payload, while memory-only tokens limit the blast radius. The visible trade-off — refreshing the page logs you out — is acceptable for a banking workbench.

pnpm test runs every workspace's Vitest suite; pnpm build builds everything (shared → ui → apps, in dependency order).

Independent deployment demo

This is the core promise of the architecture: redeploy one remote without touching the shell.

# 1. build everything once and start the four origins + BFF
pnpm build
docker compose up -d          # shell :8090, remotes :8081-8083, bff :3001

# 2. log in at http://localhost:8090 and open 轉帳中心 (transfers)

# 3. change something visible in mfe-transfers
sed -i '' 's/轉帳中心</轉帳中心 v2</' apps/mfe-transfers/src/app/wizard/transfer-wizard.ts

# 4. rebuild and restart ONLY the transfers origin
pnpm --filter mfe-transfers build
docker compose restart mfe-transfers    # ng build recreates dist/, nginx re-resolves the mount

# 5. refresh the browser — "轉帳中心 v2" appears.
#    The shell was neither rebuilt nor restarted: verify with
docker inspect -f '{{.State.StartedAt}}' microbank-shell-1

The same shell build serves both dev (:4201-4203) and docker (:8081-8083) topologies because deploy/federation.manifest.docker.json is mounted over the baked manifest — remote URLs are runtime configuration, not build-time constants.

Failure degradation drill: docker stop microbank-mfe-admin-1 (or kill the 4203 dev server), then navigate to /admin — an error card with a retry button appears while accounts and transfers remain fully functional.

BFF contract discipline

Every request/response shape is defined once as a zod schema in packages/shared/contracts/; the front-ends derive their types via z.infer, and the BFF validates with the same schema objects. Malformed input gets the unified error envelope:

{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ /* zod issues */ ] } }

Try it: curl -X POST localhost:3001/auth/login -H 'content-type: application/json' -d '{"username":"amy"}'. Staff-only routes (/admin/*) enforce the staff role server-side; lock/unlock and OTP-reset operations require a reason and append to an audit log.

AI-assisted development setup

This repository was built with Claude Code using a deliberately structured harness (visible in .claude/):

  • Skills (.claude/skills/) — four playbooks with hard stop-rules: native-federation (v4 API conventions + a verified reference doc; forbids editing federation config before checking version alignment), design-system, bff-contract (no endpoint before its zod schema exists), and stage-verify (no next stage before tests + manual checks + DEVLOG + semantic commit).
  • Hooks (.claude/settings.json) — block writes to .env/*.pem, run tsc --noEmit + Prettier after every TypeScript edit, and append every tool action to an audit log.
  • Reviewer subagent (.claude/agents/reviewer.md) — checks for illegal remote-to-remote imports, shared-version drift, and contract bypasses.
  • DEVLOG (docs/DEVLOG.md) — one entry per stage with decisions and trade-offs; the first entry is the up-front verification of the Native Federation v4 API against current docs (training-data memories of the v3 API were explicitly banned).
  • Every stage was verified end-to-end in a real browser (Playwright against system Chrome) before its semantic commit — which is exactly how the CORS preflight gap and a dist-mount pitfall were caught.

License / usage

Portfolio project. All data simulated; not affiliated with any bank.

About

Angular 微前端銀行工作台

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages