For dApps & ServicesLedger WalletContributing to Ledger WalletExplanationArchitecture Decisions

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.

PhaseStatusDescription
Legacy CodeOngoingPre-MVVM code in apps/*/src/ — being progressively migrated
MVVM PatternStandard (2+ years)ViewModel pattern in src/mvvm/ — used for new development
Feature-First ArchitectureIn ProgressNew target: root-level features/ folders

The codebase currently contains three layers of code:

LayerLocationDescription
Legacyapps/*/src/ (outside mvvm/)Pre-MVVM code, being progressively migrated
MVVMapps/*/src/mvvm/Code following the ViewModel pattern (2+ years of development)
Feature-FirstRoot 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 to features/
  • 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:

  1. 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.

  2. Highlight domain-specific code — Domain-Driven Design principles make business logic explicit. Each feature encapsulates its own domain entities within its data-layer/ subfolder.

  3. 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.

  4. Collocate synergizing files — Related code (components, data-layer, tests) lives together within each feature, rather than being scattered across global folders.

  5. 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

LayerResponsibility
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

  1. 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.

  2. 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.

  3. 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

BenefitDescription
TestabilityView components can be tested with simple prop mocking
ReusabilityViews can be reused with different ViewModels
ClarityClear separation between “what to render” and “what data to use”
DebuggingEasier 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

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.