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. The subsystem accessors themselves live on a separate ServiceRegistry interface that Application inherits, so components that only need service access (not lifecycle control) can hold a ServiceRegistry& instead.

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. It inherits the subsystem accessors from ServiceRegistry (include/xrpl/core/ServiceRegistry.h) and adds lifecycle control plus a few utilities:

The interface every subsystem is reached through is ServiceRegistry (trimmed to the accessors you'll use most):

Note there is no RPCHandler class: RPC commands are dispatched through free functions in src/xrpld/rpc/RPCHandler.h (RPC::doCommand), and the RPC/WebSocket servers are managed by ServerHandler, reached via getServerHandler().

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 (trimmed; note they are public, under a NOLINT block, and several are held by value or std::optional rather than unique_ptr):

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. The real call chain in src/xrpld/app/main/Main.cpp is: load Config → makeApplication(...) → app->setup(vm) → app->start(true) → app->run().

Phase 1: Configuration Loading

// In Main.cpp
auto config = std::make_unique<Config>();

auto configFile = vm.contains("conf") ? vm["conf"].as<std::string>() : std::string();

// config file, quiet flag.
config->setup(
    configFile, vm.contains("quiet"), vm.contains("silent"), vm.contains("standalone"));

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] - Amendments to treat as enabled locally (this is not voting)
  • [amendments] / [veto_amendments] - Amendment voting (up-vote / down-vote)

Phase 2: Application Construction

// Create the application instance
auto app =
    makeApplication(std::move(config), std::move(logs), std::make_unique<TimeKeeper>());

Constructor Sequence (ApplicationImp::ApplicationImp()): the subsystems are constructed in the member-initializer list, in member declaration order — not assigned in the constructor body. Trimmed:

Notes on the real order:

  • The first subsystem is PerfLog, with the comment "PerfLog must be started before any other threads are launched."
  • The NodeStore is created through shaMapStore_->makeNodeStore(...), not directly.
  • The Overlay is not created here — it stays a null unique_ptr until setup().
  • The relational database is also opened later, in setup(), via initRelationalDatabase().
  • Order matters! Components may depend on earlier ones.

Phase 3: Setup

if (!app->setup(vm))
    return -1;

What Happens (ApplicationImp::setup() returns bool; trimmed):

Setup also loads the last ledger state (or creates a genesis ledger in standalone mode), loads peer reservations, manifests and validator lists, and configures the ServerHandler ports. Note that amendment voting comes from the [amendments] and [veto_amendments] config sections here.

Phase 4: Start

// Start the server
app->start(true /*start timers*/);

What Happens (ApplicationImp::start(bool withTimers)):

This is where threads, sockets, and timers actually come alive — exactly the "real work" the constructor comment forbids.

Phase 5: Run

// Block until we get a stop RPC.
app->run();

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

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 blocks in run() on the isTimeToStop atomic flag until a shutdown is signalled — and then run() itself performs the teardown shown above.

Phase 6: Shutdown

// e.g. in the "stop" RPC handler
// (src/xrpld/rpc/handlers/admin/server_control/Stop.cpp)
context.app.signalStop("RPC");

signalStop() does not perform the shutdown. Its entire job is to set the stop flag and wake the main thread (ApplicationImp::signalStop()):

It is called from the stop admin RPC, from the POSIX signal handler (signalStop("Signal: " + to_string(signum))), and even from subsystems themselves (for example when the transaction database runs out of space). The actual teardown then runs at the end of run().

Shutdown Order: The stop calls at the end of run() are not simply the reverse of construction order — jobQueue_ is among the first members constructed yet stops third, while overlay_ is created last (in setup()) yet stops fourth. The code's own comment says it best: "The order of these stop calls is delicate. Re-ordering them risks undefined behavior."

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 (from src/xrpld/app/ledger/LedgerMaster.h):

tryAdvance() is the entry point for advancing the validated ledger (it schedules a JtAdvance job). Fetching missing ledgers is not LedgerMaster's job: that belongs to InboundLedgers, reached via app.getInboundLedgers().

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 (from include/xrpl/server/NetworkOPs.h):

Overlay

Purpose: Manages peer-to-peer networking layer.

Key Responsibilities:

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

Access: app.getOverlay()

Important Methods (from src/xrpld/overlay/Overlay.h):

Note there is no generic broadcast(Message): only proposals and validations are broadcast to every peer; other traffic goes through relay/send paths.

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 (from src/xrpld/app/misc/TxQ.h):

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 (from include/xrpl/nodestore/Database.h):

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 (embedded) — the only relational backend. ApplicationImp holds a std::optional<SQLiteDatabase> directly; the PostgreSQL backend was removed together with reporting mode.

Validations

Purpose: Manages validator signatures on ledger closes.

Key Responsibilities:

  • Collect validations from validators
  • Track which validations are current, trusted, and full
  • Count trusted validations for a ledger (the raw input to validation decisions)
  • Publish validation stream

Access: app.getValidations() — returns RCLValidations&, an alias for Validations<RCLValidationsAdaptor> (the generic engine in src/xrpld/consensus/Validations.h with an RCL-specific adaptor).

Important Methods (from src/xrpld/consensus/Validations.h, trimmed to declarations):

Incoming validations from the network enter through the free function handleNewValidation (src/xrpld/app/consensus/RCLValidations.h):

void
handleNewValidation(
    Application& app,
    std::shared_ptr<STValidation> const& val,
    std::string const& source,
    BypassAccept const bypassAccept = BypassAccept::No,
    std::optional<beast::Journal> j = std::nullopt);

Two related responsibilities live elsewhere: validator key rotations (manifests) are tracked by ManifestCache, reached via app.getValidatorManifests() and app.getPublisherManifests(), and the quorum policy (how many trusted validators are needed) lives in ValidatorList, reached via app.getValidators().


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. From include/xrpl/core/Job.h (trimmed) — read the comment carefully: earlier entries have LOWER priority, so the highest-priority job types sit at the bottom of the enum:

So the consensus-critical types — JtValidationT (trusted validations), JtAccept (accept a consensus ledger), and JtProposalT (trusted proposals) — are near the bottom, i.e. among the highest priorities, with JtAdmin the highest-priority dispatched type. JtPack and JtPuboldledger at the top are the two lowest. JtPeer and JtDisk are never queued at all; they exist only for load measurement.

Submitting Jobs

Components submit work to the job queue via addJob. The handler is a lambda taking no arguments — the addJob template (include/xrpl/core/JobQueue.h) enforces this at compile time:

template <
    typename JobHandler,
    typename = std::enable_if_t<std::is_same_v<decltype(std::declval<JobHandler&&>()()), void>>>
bool
addJob(JobType type, std::string const& name, JobHandler&& jobHandler)

A real call, from NetworkOPsImp::submitTransaction (src/xrpld/app/misc/NetworkOPs.cpp):

jobQueue_.addJob(JtTransaction, "SubmitTxn", [this, tx]() {
    auto t = tx;
    processTransaction(t, false, false, FailHard::No);
});

(Outside NetworkOPs, get the queue with app.getJobQueue() first.)

Job Priority and Scheduling

Priority Levels (from the real enum order — higher in this list = processed first):

  • Highest: JtAdmin (administrative operations), then the NetworkOPs timer/cluster jobs
  • Consensus-critical: JtProposalT, JtAccept, JtValidationT (trusted proposals, ledger accept, trusted validations)
  • Ledger progression: JtPubledger, JtAdvance, JtLedgerData, JtTransaction
  • Lower: untrusted validations/proposals, pathfinding updates, sweeps
  • Lowest dispatched: client and RPC jobs (JtClient*, JtRpc), JtPuboldledger, JtPack

Scheduling Algorithm:

  1. Worker threads pick the highest-priority waiting job
  2. Each job type also has a per-type queue limit (include/xrpl/core/JobTypes.h) — this, not priority alone, is what protects the node under load
  3. Each type carries target average/peak latencies used for load measurement

A slice of the real table (JobTypes.h):

The name column is what you'll see in the logs (e.g. transaction, advanceLedger, publishNewLedger), not the enum constant.

Job Queue Configuration

In xrpld.cfg:

[node_size]
# Influences the worker thread count (and many cache sizes).
# The default is tiny.
medium

[workers]
# Optional: explicitly set the job queue thread count.

The real thread-count logic is the lambda that constructs the JobQueue in ApplicationImp's member-initializer list (src/xrpld/app/main/Application.cpp):

So: 1 thread in standalone mode, the [workers] value if set, otherwise a core-count-derived number that only grows for large/huge node sizes on machines with enough cores.


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

Be careful with [features]: it force-enables the listed amendments locally regardless of the network's amendment state, which can put a server out of sync with the network. Amendment voting is configured with [amendments] and [veto_amendments], which ApplicationImp::setup() feeds into the AmendmentTable.

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. Here is the real NetworkOPsImp::submitTransaction (src/xrpld/app/misc/NetworkOPs.cpp, trimmed) — notice it makes direct calls into other subsystems (HashRouter, LedgerMaster) to validate, but deliberately does not apply or broadcast inline; it hands the actual processing to the job queue:

Pattern 2: Job Queue for Asynchronous Work

For work that should not block the caller. The real LedgerMaster::tryAdvance (src/xrpld/app/ledger/detail/LedgerMaster.cpp, trimmed):

Pattern 3: Event Publication

Ledger and transaction events are published to subscribers through NetworkOPs, which implements InfoSub::Source (include/xrpl/server/InfoSub.h). Subscribers (WebSocket clients, internal consumers) register through the sub* methods; the publisher side pushes events with the pub* methods:

Pattern 4: Callback Registration

Components register callbacks for specific events, usually at construction time. Two real examples from ApplicationImp's member-initializer list (src/xrpld/app/main/Application.cpp):

// PerfLog must be started before any other threads are launched.
, perfLog_(
      perf::makePerfLog(
          perf::setupPerfLog(config_->section("perf"), config_->configDir),
          *this,
          logs_->journal("PerfLog"),
          [this] { signalStop("PerfLog"); }))
, inboundTransactions_(makeInboundTransactions(
      *this,
      collectorManager_->collector(),
      [this](std::shared_ptr<SHAMap> const& set, bool fromAcquire) {
          gotTXSet(set, fromAcquire);
      }))

Codebase Deep Dive

Key Files and Directories

Application Core:

Job Queue:

Configuration:

Subsystem Implementations:

Code Navigation Tips

Finding Application Creation

Start in Main.cpp (trimmed):

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 (from src/xrpld/app/ledger/detail/LedgerMaster.cpp):

app_.getJobQueue().addJob(JtAdvance, "AdvanceLedger", [this]() {
    // ...
});

# Submit a payment
xrpld submit '{
  "TransactionType": "Payment",
  "Account": "...",
  "Destination": "...",
  "Amount": "1000000"
}'

Watch the logs for:

  • JtTransaction jobs being queued (they appear in the logs under the name transaction, from JobTypes.h)
  • Job processing time
  • Queue depth changes

Step 5: Manually close a ledger

xrpld ledger_accept

Observe jobs related to ledger close:

  • JtAdvance (logged as advanceLedger) - Advance validated/acquired ledgers
  • JtPubledger (logged as publishNewLedger) - Publish a fully-accepted ledger
  • JtUpdatePf (logged as updatePaths) - Update pathfinding requests

Part 3: Add Custom Logging

Step 1: Modify Application.cpp

The subsystems are constructed in ApplicationImp's member-initializer list, not in the constructor body, so you can't wrap each construction with log lines. Instead, add logging at the start of the lifecycle methods (the journal member is journal_):

bool
ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
{
    JLOG(journal_.info()) << "Entering setup";
    // ... existing code ...

Add a similar line at the top of start(bool withTimers) and run().

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 the order in which the lifecycle phases run, interleaved with the existing startup logs.

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, start, 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 (the accessors live on ServiceRegistry, which Application inherits)
  • Interface and ApplicationImp live in src/xrpld/app/main
  • Major subsystems: LedgerMaster, NetworkOPs, Overlay, NodeStore, JobQueue
  • Reach them through accessors: app.getLedgerMaster(), app.getOverlay(), app.getNodeStore()
  • Background work goes through the JobQueue (include/xrpl/core/JobQueue.h): app.getJobQueue().addJob(...) with a no-argument lambda, priority-scheduled (later JobType enum entries = higher priority)
  • The lifecycle is setup(vm) → start(true) → run(); signalStop() only sets a flag, and the teardown runs at the end of run() in an explicit, delicate order — not simply the reverse of construction
  • All behaviour is configured from xrpld.cfg, parsed into Config (src/xrpld/core); [features] enables amendments locally, while voting uses [amendments]/[veto_amendments]
  • 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 over half a million lines of C++. Next: navigating the rippled codebase without getting lost.

Assignments

0 of 2 complete

XRPL Academy © 2026