Architecture decision ADR-001

Adopt Modular Monolith (Service-Based) Architecture Over Microservices for Phase 1

Accepted · 2026-05-10

Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.

Date: 2026-05-10
Status: Accepted
Deciders: Daniel Meza


Context and Problem Statement

Urza's Wallet launches with three product verticals (credit cards, pension funds, home loans) and a small initial team. The NerdWallet reference architecture uses a full microservices approach with 10+ independently deployed services (Content Service, Product Catalog, Offer Ingestion, Ranking, Calculator, User Profile, CIAM, Account Aggregation, Consent, Conversion Tracking, Notification). We need to decide whether to adopt that model at launch or use a simpler architecture that fits our team size, operational constraints, and Railway deployment model.

The core tension: microservices provide independent deployability and fault isolation per domain, but they impose significant operational overhead (service discovery, inter-service networking, distributed tracing, per-service CI/CD (Continuous Integration/Continuous Deployment) pipelines, multiple databases). A modular monolith provides the same codebase clarity through module boundaries while reducing operational complexity — at the cost of coupled deployability.


Decision Drivers


Considered Options

  1. Full microservices — One Railway service per domain (product catalog, user profile, ingestion, notification, etc.), each with its own database.
  2. Service-based architecture (2 deployable units) — Next.js web app (with BFF (Backend-for-Frontend)) + Backend API Service (with internal modules), shared PostgreSQL.
  3. Layered monolith — Single deployable unit containing all logic, tightly layered but not modular.
  4. Modular monolith with future service extraction — Same as option 2 but with explicit module boundaries designed for extraction when a specific domain needs independent scaling.

Decision Outcome

Option 2: Service-based architecture with 2 deployable units is adopted for Phase 1.

The two deployable units are:

  1. Next.js Web App — handles SSG (Static Site Generation)/ISR (Incremental Static Regeneration)/SSR (Server-Side Rendering) rendering, Clerk session management, and acts as the BFF by aggregating API calls server-side.
  2. Backend API Service — Node.js/TypeScript Express app containing four internal modules: Product Comparison, User Profile, Content/CMS, and Data Ingestion control plane. Shares a single PostgreSQL database.

A third non-HTTP unit exists: the Data Ingestion Service (Railway cron job), which is a separate process but shares the same PostgreSQL database and has no public network interface.

Internal module boundaries within the Backend API Service are enforced via directory structure and import rules (no cross-module direct database access — each module owns its own repository class). These boundaries are designed for eventual extraction into separate services if a specific domain's load or team ownership warrants it.


Positive Consequences

Negative Consequences / Trade-offs


Pros and Cons of the Options

Option Pros Cons
Full microservices True fault isolation; independent deployability per domain 10x operational overhead; premature for launch traffic; Railway not designed for service meshes
Service-based (chosen) Low ops; module clarity; extractable later; shared DB simplifies queries Coupled deploy; shared DB scaling ceiling; module drift risk
Layered monolith Simplest structure No module boundaries; extraction later is a full rewrite
Modular monolith (single deploy) Maximum simplicity Web and API coupled to the same deploy; can't scale independently

References