Architecture Decisions
This page explains the reasoning behind the Ledger Wallet architecture: why we adopted a feature-first structure, why we use the MVVM pattern, and how the codebase is evolving through migration.
The Migration Journey
The Ledger Wallet codebase is undergoing a significant architectural evolution. Understanding the history helps make sense of the current state.
| Phase | Status | Description |
|---|---|---|
| Legacy Code | Ongoing | Pre-MVVM code in apps/*/src/ — being progressively migrated |
| MVVM Pattern | Standard (2+ years) | ViewModel pattern in src/mvvm/ — used for new development |
| Feature-First Architecture | In Progress | New target: root-level features/ folders |
The codebase currently contains three layers of code:
| Layer | Location | Description |
|---|---|---|
| Legacy | apps/*/src/ (outside mvvm/) | Pre-MVVM code, being progressively migrated |
| MVVM | apps/*/src/mvvm/ | Code following the ViewModel pattern (2+ years of development) |
| Feature-First | Root features/ | New architecture target |
This means:
- New features should be written in the
features/folder at the monorepo root - MVVM code in
src/mvvm/follows the ViewModel pattern and will be progressively migrated tofeatures/ - Legacy code predates MVVM and should be migrated when touched significantly
libs/folder contains legacy shared libraries being progressively migrated into features
Why Feature-First Architecture
The feature-first architecture is inspired by the C4 model and follows a deliberate philosophy:
- Root level contains everything connected to the exterior (apps, platform concerns)
- One level deep contains core business capabilities (features)
- Two levels deep contains feature-related business logic
Design Goals
The architecture was designed to address specific pain points in the previous structure:
-
Ease codebase discovery — A clear, intuitive folder structure helps new contributors find what they need. Instead of navigating deep into platform-specific folders, features are visible at the root level.
-
Highlight domain-specific code — Domain-Driven Design principles make business logic explicit. Each feature encapsulates its own domain entities within its
data-layer/subfolder. -
Balance folder depth — The previous structure suffered from both deep nesting and flat file dumps. The new structure aims for a shallow, consistent depth across the codebase.
-
Collocate synergizing files — Related code (components, data-layer, tests) lives together within each feature, rather than being scattered across global folders.
-
Unify mobile and desktop — Instead of maintaining separate codebases for mobile and desktop, features use platform-specific file extensions (
.web.ts,.native.ts) to keep code close together while respecting platform differences.
Layer Responsibilities
| Layer | Responsibility |
|---|---|
apps/ | Platform infrastructure — routing, state manager, observability. No business logic. |
features/ | Self-contained business features with high cohesion, low coupling |
libs/ | Legacy shared libraries — being progressively migrated into features |
The strict import boundaries between these layers enforce separation of concerns. Apps wire features together but should not contain business logic. This prevents circular dependencies and makes it possible to reason about each layer independently.
Why MVVM
The MVVM (Model-View-ViewModel) pattern was adopted to address a specific problem: components that mix UI rendering with business logic (Redux selectors, API calls, state transformations) become difficult to test and maintain.
Key Principles
-
Separation of concerns — UI components should be pure and receive data via props. All logic that connects to outer layers (store, network, etc.) lives in the ViewModel hook.
-
Testability — The View component can be tested in isolation with mock props, without needing to mock Redux, API calls, or any other external dependency. The ViewModel can be tested independently as a hook.
-
Props injection — The ViewModel hook result is spread as props to the View. This is a deliberate design choice: by injecting props rather than calling the hook inside the component body, you get a clear boundary between “what data to use” and “what to render.”
Benefits
| Benefit | Description |
|---|---|
| Testability | View components can be tested with simple prop mocking |
| Reusability | Views can be reused with different ViewModels |
| Clarity | Clear separation between “what to render” and “what data to use” |
| Debugging | Easier to isolate issues to either UI or logic |
MVVM is not used everywhere — simple presentational components, pure utility components, and components that only receive props from parents don’t need it. The pattern is reserved for components that connect to Redux, make API calls, or contain complex business logic.
See also
- Architecture reference — Complete folder structure, import rules, platform extensions
- MVVM Pattern reference — Implementation details and when to use
- How to Structure a Feature — Step-by-step guide for applying this architecture