advanced 60 min

The Application layer

How the `Application` class wires rippled together — the central orchestrator that owns and coordinates every subsystem, plus the job queue and configuration.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain how the Application acts as a dependency-injection container / service locator.
  • Trace rippled's startup, run and shutdown sequence.
  • Identify the major subsystems (LedgerMaster, NetworkOPs, Overlay, NodeStore…) and how to reach them.
  • Understand the job queue and how work is scheduled.
Complete this module by mentor review and a quiz. Jump to assessment

Introduction

≈60 min · Advanced · builds on Build & run your own xrpld node

Now that your node runs, let's see what actually holds it together. At the center of rippled sits one object (the Application class) that owns and wires up every subsystem, from the ledger to the network to storage. In this module you'll trace how the node starts up, runs and shuts down, meet the major subsystems and the job queue that schedules their work, and learn how any component reaches another. It's the map you'll keep coming back to as the codebase gets deeper.


The Application Class Architecture

In brief: the single object that owns every subsystem and hands them out on request (a service locator).

Design Philosophy

The Application class follows several key design principles that make Rippled maintainable and extensible:

Single Point of Coordination: Instead of components directly creating and managing their dependencies, everything flows through the Application. This centralization makes it easy to understand system initialization and component relationships.

Dependencies through the Application: rippled predominantly uses the service locator style: subsystems receive Application& and ask it for what they need (app.getLedgerMaster()), rather than receiving each dependency in their constructor. A few components do take explicit constructor dependencies, but Application& is the norm; it trades some testability for one obvious place to find everything.

Interface-Based Design: The Application class implements the Application interface, allowing for different implementations (production, test, mock) without changing dependent code.

Lifetime Management: The Application controls the creation, initialization, and destruction of all major subsystems, ensuring proper startup/shutdown sequences.

Application Interface

The Application interface is defined in src/xrpld/app/main/Application.h:

The interface every subsystem is reached through:

ApplicationImp Implementation

The concrete implementation ApplicationImp is in src/xrpld/app/main/Application.cpp. This class:

  • Implements all interface methods
  • Owns all major subsystem objects
  • Manages initialization order
  • Coordinates shutdown
  • Provides cross-cutting services

Key Member Variables:

Key idea. Almost nothing in rippled constructs its own dependencies; it asks the Application for them. Find the Application and you can reach the whole node.


Initialization and Lifecycle

In brief: how the node starts up, runs, and shuts down, in order.

Startup Sequence

Understanding the startup sequence is crucial for debugging initialization issues and understanding component dependencies.

Phase 1: Configuration Loading

// In Main.cpp
auto config = std::make_unique<Config>();
if (!config->setup(configFile, quiet))
{
    // Configuration failed
    return -1;
}

What Happens:

  • Parse xrpld.cfg configuration file
  • Load validator list configuration
  • Set up logging configuration
  • Validate configuration parameters
  • Apply defaults for unspecified options

Configuration Sections:

  • [server] - Server ports and interfaces
  • [node_db] - NodeStore database configuration
  • [node_size] - Performance tuning parameters
  • [validation_seed] - Validator key configuration
  • [ips_fixed] - Fixed peer connections
  • [features] - Amendment votes

Phase 2: Application Construction

// Create the application instance
auto app = make_Application(
    std::move(config),
    std::move(logs),
    std::move(timeKeeper));

Constructor Sequence (ApplicationImp::ApplicationImp()):

Phase 3: Setup

app->setup();

What Happens (ApplicationImp::setup()):

Phase 4: Run

app->run();

Main Event Loop (ApplicationImp::run()):

What Runs:

  • Job queue processes queued work
  • Overlay network handles peer connections
  • Consensus engine processes rounds
  • NetworkOPs coordinates operations
  • RPC handlers process client requests

All work happens in background threads managed by various subsystems. The main thread simply waits for a shutdown signal.

Phase 5: Shutdown

app->signalStop();

Graceful Shutdown (ApplicationImp::signalStop()):

Shutdown Order: Components are stopped in reverse order of their creation to ensure dependencies are still available when each component shuts down.

Complete Lifecycle Diagram

The application lifecycle: program start, load configuration (xrpld.cfg), create the Application instance, construct the subsystems, setup phase, run phase (the main loop and steady state), shutdown signal, graceful shutdown, program exit


Subsystem Coordination

In brief: how components find and call each other through the Application.

The Service Locator Pattern

The Application acts as a service locator, allowing any component to access any other component through the app reference:

Major Subsystems

LedgerMaster

Purpose: Manages the chain of validated ledgers and coordinates ledger progression.

Key Responsibilities:

  • Track current validated ledger
  • Build candidate ledgers for consensus
  • Synchronize ledger history
  • Maintain ledger cache
  • Coordinate with consensus engine

Access: app.getLedgerMaster()

Important Methods:

// Get current validated ledger
std::shared_ptr<Ledger const> getValidatedLedger();

// Get closed ledger (not yet validated)
std::shared_ptr<Ledger const> getClosedLedger();

// Advance to new ledger
void advanceLedger();

// Fetch missing ledgers
void fetchLedger(LedgerHash const& hash);

NetworkOPs

Purpose: Coordinates network operations and transaction processing.

Key Responsibilities:

  • Process submitted transactions
  • Manage transaction queue
  • Coordinate consensus participation
  • Track network state
  • Publish ledger close events

Access: app.getOPs()

Important Methods:

// Submit transaction
void submitTransaction(std::shared_ptr<STTx const> const& tx);

// Process transaction
void processTransaction(
    std::shared_ptr<Transaction>& transaction,
    bool trusted,
    bool local);

// Get network state
OperatingMode getOperatingMode();

Overlay

Purpose: Manages peer-to-peer networking layer.

Key Responsibilities:

  • Peer discovery and connection
  • Message routing
  • Network topology maintenance
  • Bandwidth management

Access: app.overlay()

Important Methods:

// Send message to all peers
void broadcast(std::shared_ptr<Message> const& message);

// Get active peer count
std::size_t size() const;

// Connect to specific peer
void connect(std::string const& ip);

TxQ (Transaction Queue)

Purpose: Manages transaction queuing when network is busy.

Key Responsibilities:

  • Queue transactions during high load
  • Fee-based prioritization
  • Account-based queuing limits
  • Transaction expiration

Access: app.getTxQ()

Important Methods:

// Check if transaction can be added
std::pair<TER, bool> 
apply(Application& app, OpenView& view, STTx const& tx);

// Get queue status
Json::Value getJson();

NodeStore

Purpose: Persistent storage for ledger data.

Key Responsibilities:

  • Store ledger state nodes
  • Provide efficient retrieval
  • Cache frequently accessed data
  • Support different backend databases (RocksDB, NuDB)

Access: app.getNodeStore()

Important Methods:

// Store ledger node
void store(
    NodeObjectType type,
    Blob const& data,
    uint256 const& hash);

// Fetch ledger node
std::shared_ptr<NodeObject> 
fetch(uint256 const& hash);

RelationalDatabase

Purpose: SQL database for indexed data and historical queries.

Key Responsibilities:

  • Store transaction metadata
  • Maintain account transaction history
  • Support RPC queries (account_tx, tx)
  • Ledger header storage

Access: app.getRelationalDatabase()

Database Types:

  • SQLite (default, embedded)
  • PostgreSQL (production deployments)

Validations

Purpose: Manages validator signatures on ledger closes.

Key Responsibilities:

  • Collect validations from validators
  • Track validator key rotations (manifests)
  • Determine ledger validation quorum
  • Publish validation stream

Access: app.getValidations()

Important Methods:

// Add validation
void addValidation(STValidation const& val);

// Get validation for ledger
std::vector<std::shared_ptr<STValidation>>
getValidations(LedgerHash const& hash);

// Check if ledger is validated
bool hasQuorum(LedgerHash const& hash);

Job Queue System

In brief: how background work is queued, prioritized, and run across worker threads.

Purpose and Design

The job queue is Rippled's work scheduling system. Instead of each subsystem creating its own threads, work is submitted as jobs to a centralized queue processed by a thread pool. This provides:

  • Centralized thread management: Easier to control thread count and CPU usage
  • Priority-based scheduling: Critical jobs processed before low-priority ones
  • Visibility: Easy to monitor what work is queued
  • Deadlock prevention: Structured concurrency patterns

Job Types

Jobs are categorized by type, which determines priority:

Submitting Jobs

Components submit work to the job queue:

// Get job queue reference
JobQueue& jobs = app.getJobQueue();

// Submit a job
jobs.addJob(
    jtTRANSACTION,  // Job type
    "processTx",     // Job name (for logging)
    [this, tx](Job&) // Job function
    {
        // Do work here
        processTransaction(tx);
    });

Job Priority and Scheduling

Priority Levels:

  • Critical: Consensus, validations (must not be delayed)
  • High: Transaction processing, ledger advancement
  • Medium: RPC requests, client operations
  • Low: Maintenance, administrative tasks

Scheduling Algorithm:

  1. Jobs sorted by priority and submission time
  2. Worker threads pick highest priority job
  3. Long-running jobs can be split into chunks
  4. System monitors queue depth and adjusts behavior

Job Queue Configuration

In xrpld.cfg:

[node_size]
# Affects worker thread count
tiny      # 1 thread
small     # 2 threads  
medium    # 4 threads (default)
large     # 8 threads
huge      # 16 threads

Thread count is also influenced by CPU core count:

// Typically: max(2, std::thread::hardware_concurrency() - 1)

Configuration Management

In brief: how the node reads its config and exposes it to every subsystem.

Configuration File Structure

The xrpld.cfg file controls all aspects of server behavior. The Application loads and provides access to this configuration.

Example Configuration

Accessing Configuration

Components access configuration through the Application:

Runtime Configuration

Some settings can be adjusted at runtime via RPC:

# Change log verbosity
xrpld log_level partition severity

# Connect to peer
xrpld connect ip:port

# Get server info
xrpld server_info

Component Interaction Patterns

Pattern 1: Direct Method Calls

Most common pattern, components call each other's methods:

Pattern 2: Job Queue for Asynchronous Work

For work that should not block the caller:

Pattern 3: Event Publication

Components publish events that others subscribe to:

Pattern 4: Callback Registration

Components register callbacks for specific events:

// Register callback
app_.getLedgerMaster().onConsensusReached(
    [this](std::shared_ptr<Ledger const> const& ledger)
    {
        handleConsensusLedger(ledger);
    });

Codebase Deep Dive

Key Files and Directories

Application Core:

Job Queue:

Configuration:

Subsystem Implementations:

Code Navigation Tips

Finding Application Creation

Start in Main.cpp:

Tracing Component Access

Follow how components access each other:

// In any component
void MyComponent::work()
{
    // Access through app_
    auto& ledgerMaster = app_.getLedgerMaster();  // → ApplicationImp::getLedgerMaster()
                                                   // → return *ledgerMaster_;
}

Understanding Job Submission

Find job submissions:

# Search for addJob calls
grep -r "addJob" src/xrpld/app/

Example:

app_.getJobQueue().addJob(jtTRANSACTION, "processTx", [&](Job&) {
    // Job code
});

Submit a payment

xrpld submit '{ "TransactionType": "Payment", "Account": "...", "Destination": "...", "Amount": "1000000" }'


Watch the logs for:

* `jtTRANSACTION` jobs being queued
* Job processing time
* Queue depth changes

**Step 5**: Manually close a ledger

```bash
xrpld ledger_accept

Observe jobs related to ledger close:

  • jtADVANCE - Advance to next ledger
  • jtPUBLEDGER - Publish ledger
  • jtUPDATE_PF - Update path finding

Part 3: Add Custom Logging

Step 1: Modify Application.cpp

Add logging to track component initialization:

ApplicationImp::ApplicationImp(/* ... */)
{
    JLOG(j_.info()) << "Creating JobQueue...";
    jobQueue_ = std::make_unique<JobQueue>(/* ... */);
    JLOG(j_.info()) << "JobQueue created";
    
    JLOG(j_.info()) << "Creating NodeStore...";
    nodeStore_ = NodeStore::Manager::make(/* ... */);
    JLOG(j_.info()) << "NodeStore created";
    
    // Add similar logs for other components
}

Step 2: Recompile

cd rippled/build
cmake --build . --target xrpld

Step 3: Run and observe

./xrpld --conf=xrpld.cfg --standalone

You should see your custom log messages showing component creation order.

Analysis Questions

Answer these based on your exploration:

  1. What's the first subsystem created?
  • Why does it need to be first?
  1. How does the job queue decide which job to process next?
  • What factors influence priority?
  1. What happens if a job throws an exception?
  • Find the exception handling code
  1. How many jobs are queued during a typical ledger close?
  • Count from your logs
  1. What's the relationship between Application and ApplicationImp?
  • Why use an interface?
  1. How would you add a new subsystem?
  • What's the process?
  • Where would you add it?

Additional Resources

Official Documentation

Codebase References

  • Transactors - How transactions are processed
  • Consensus Engine - How consensus integrates with Application
  • Codebase Navigation - Finding your way around the code

Summary

This module explained how the Application class holds rippled together. It is the central orchestrator that constructs and owns every subsystem, from LedgerMaster and NetworkOPs to the Overlay and NodeStore, and hands them out on request (a service-locator pattern). You saw the node's startup, run, and shutdown sequence, how components reach one another through the Application, and how background work is scheduled on the job queue.

To remember:

  • Application is the service locator: components ask it for subsystems instead of constructing their own
  • Interface and ApplicationImp live in src/xrpld/app/main
  • Major subsystems: LedgerMaster, NetworkOPs, Overlay, NodeStore, JobQueue
  • Reach them through accessors: app.getLedgerMaster(), app.overlay(), app.getNodeStore()
  • Background work goes through the JobQueue (include/xrpl/core/JobQueue.h): app.getJobQueue().addJob(...), priority-scheduled
  • Subsystems are constructed in dependency order at startup; shutdown unwinds in reverse
  • All behaviour is configured from xrpld.cfg, parsed into Config (src/xrpld/core)
  • Watch out: touching a subsystem before the Application finishes construction is the classic startup crash; respect the init order

Next up. You know who owns every subsystem; now you need to find them in two million lines of C++. Next: navigating the rippled codebase without getting lost.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026