# Best Practices and Patterns

### Writing Maintainable, Performant RPC Handlers Following Industry Standards

[← Back to Building and Integrating Custom RPC Handlers](/core-dev-bootcamp/module07.md)

***

## Introduction

The difference between a working handler and a production-ready handler lies in **adherence to best practices**. This section distills lessons from Rippled's codebase and years of RPC API development into concrete, actionable guidelines.

You'll learn code style conventions, common pitfalls to avoid, performance optimization strategies, documentation standards, and maintenance best practices that will make your handlers robust, efficient, and easy to maintain.

***

## Code Style Guidelines

### Naming Conventions

**Handlers**:

```cpp
// Pattern: doPascalCaseCommandName
Json::Value doAccountInfo(RPC::JsonContext& context);
Json::Value doLedgerRequest(RPC::JsonContext& context);
Json::Value doSubmitTransaction(RPC::JsonContext& context);

// Files: PascalCase.cpp and .h
// Location: src/xrpld/rpc/handlers/AccountInfo.cpp
```

**Helper Functions**:

```cpp
// camelCase for utility functions
bool validateAccountAddress(std::string_view address);
STAmount parseAmountField(Json::Value const& field);
std::shared_ptr<ReadView const> getLedgerForRequest(RPC::JsonContext& context);
```

**Member Variables**:

```cpp
class MyClass {
private:
    std::shared_ptr<ReadView const> ledger_;     // Trailing underscore
    unsigned int pageSize_;                       // Trailing underscore
    bool isValid_;                                // Boolean prefix with 'is'
};
```

**Constants**:

```cpp
// UPPER_CASE for constants
constexpr unsigned int MAX_PAGE_SIZE = 1000;
constexpr unsigned int MIN_PAGE_SIZE = 1;
constexpr std::string_view DEFAULT_LEDGER_INDEX = "validated";

// In headers, scoped to types
struct Config {
    static constexpr unsigned int DEFAULT_TIMEOUT_MS = 5000;
    static constexpr unsigned int MAX_CONNECTIONS = 1000;
};
```

### Formatting Standards

**Indentation and Spacing**:

```cpp
// Use 4 spaces for indentation (no tabs)
if (condition) {
    doSomething();
}

// Blank line between logical sections
void myFunction(RPC::JsonContext& context)
{
    // Input validation
    if (!context.params.isMember("account")) {
        return rpcError(rpcINVALID_PARAMS);
    }

    // Main logic
    auto account = parseBase58<AccountID>(
        context.params["account"].asString()
    );

    // Response building
    Json::Value result;
    result[jss::status] = jss::success;

    return result;
}
```

**Line Length**:

```cpp
// Aim for 80 characters, absolute maximum 100
// Break long lines appropriately

// Good: breaks at logical points
auto const result = doComplexCalculation(
    firstParameter,
    secondParameter,
    thirdParameter);

// Good: breaks function calls
context.netOps.broadcastMessage(
    messageType,
    transactionData,
    sourceAddress);

// Avoid: hard-to-read line breaks
auto const x = someFunc(p1, p2, p3) + anotherFunc(p4, p5) * mathOperation(p6);
```

**Braces Style**:

```cpp
// Rippled uses Stroustrup style
void myFunction()
{
    if (condition) {
        doSomething();
    }
    else {
        doSomethingElse();
    }
}

// Not:
if (condition)
{
    doSomething();
}
```

***

## Common Pitfalls to Avoid

### Pitfall 1: Trusting Client Input

```cpp
// BAD: Assumes client input is valid
Json::Value doBAD(RPC::JsonContext& context)
{
    std::string account = context.params["account"].asString();
    // Crashes if "account" doesn't exist or isn't a string
}

// GOOD: Validates all inputs
Json::Value doGood(RPC::JsonContext& context)
{
    if (!context.params.isMember("account")) {
        return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
    }

    if (!context.params["account"].isString()) {
        return rpcError(rpcINVALID_PARAMS, "'account' must be string");
    }

    std::string account = context.params["account"].asString();
}
```

### Pitfall 2: Not Checking Ledger Availability

```cpp
// BAD: Uses ledger without checking
Json::Value doBAD(RPC::JsonContext& context)
{
    auto entry = context.ledger->read(keylet::account(id));
    // Crashes if context.ledger is null
}

// GOOD: Validates ledger first
Json::Value doGood(RPC::JsonContext& context)
{
    if (!context.ledger) {
        return rpcError(rpcNO_CURRENT);
    }

    auto entry = context.ledger->read(keylet::account(id));
    if (!entry) {
        return rpcError(rpcACT_NOT_FOUND);
    }
}
```

### Pitfall 3: Exposing Implementation Details

```cpp
// BAD: Leaks internal information
catch (DatabaseException const& ex) {
    return rpcError(rpcINTERNAL,
        "SQL query failed: " + std::string(ex.what()));
}

// GOOD: Generic error message
catch (DatabaseException const& ex) {
    JLOG(context.app.journal("RPC"))
        << "Database error in handler: " << ex.what();

    return rpcError(rpcINTERNAL);
}
```

### Pitfall 4: Ignoring Resource Limits

```cpp
// BAD: No limits on memory usage
Json::Value results(Json::arrayValue);
for (auto const& item : hugeLedgerIterator) {
    results.append(item);  // Unbounded memory growth
}

// GOOD: Enforce reasonable limits
Json::Value results(Json::arrayValue);
unsigned int count = 0;
for (auto const& item : hugeLedgerIterator) {
    if (count >= MAX_RESULTS) break;
    results.append(item);
    count++;
}
```

### Pitfall 5: Not Handling Concurrent Access

```cpp
// BAD: Non-thread-safe modification
static std::vector<std::string> cache;  // Shared mutable state

Json::Value doBAD(RPC::JsonContext& context)
{
    cache.push_back(context.params["value"].asString());
    // Race condition if multiple threads call this
}

// GOOD: Thread-safe access
std::mutex cacheMutex;
std::vector<std::string> cache;

Json::Value doGood(RPC::JsonContext& context)
{
    {
        std::lock_guard<std::mutex> lock(cacheMutex);
        cache.push_back(context.params["value"].asString());
    }
    // Or use thread-local storage if appropriate
}
```

### Pitfall 6: Missing Error Response Fields

```cpp
// BAD: Incomplete error response
if (error) {
    return rpcError(rpcINVALID_PARAMS);
}

// GOOD: Complete error response with helpful message
if (error) {
    return rpcError(rpcINVALID_PARAMS,
        "Account must be in valid base58 format");
}
```

### Pitfall 7: Silent Failures

```cpp
// BAD: Silently ignores errors
if (parseAmount(params["amount"], amount)) {
    // Error silently dropped
}

// GOOD: Explicit error handling
if (!parseAmount(params["amount"], amount)) {
    return rpcError(rpcINVALID_PARAMS, "Invalid amount format");
}
```

### Pitfall 8: Not Checking Permission Requirements

```cpp
// BAD: No role check for sensitive operation
Json::Value doBAD(RPC::JsonContext& context)
{
    // This should require ADMIN role but doesn't check
    deleteAccount(accountID);
}

// GOOD: Explicit role validation
Json::Value doGood(RPC::JsonContext& context)
{
    // Handler registered with Role::ADMIN requirement
    // AND/OR explicit check:
    if (context.role < Role::ADMIN) {
        return rpcError(rpcNO_PERMISSION);
    }

    deleteAccount(accountID);
}
```

***

## Performance Considerations

### Ledger Access Optimization

```cpp
// BAD: Multiple ledger lookups
std::shared_ptr<ReadView const> ledger1;
RPC::lookupLedger(ledger1, context);

std::shared_ptr<ReadView const> ledger2;
RPC::lookupLedger(ledger2, context);  // Redundant

// GOOD: Reuse single ledger reference
std::shared_ptr<ReadView const> ledger;
auto const result = RPC::lookupLedger(ledger, context);

if (!ledger) return result;

// Use 'ledger' multiple times
```

### Query Optimization

```cpp
// BAD: Query for every item
for (auto const& accountID : accounts) {
    auto entry = ledger->read(keylet::account(accountID));
    // Many individual reads
}

// GOOD: Batch queries if possible
std::vector<std::shared_ptr<SLE const>> entries;
for (auto const& accountID : accounts) {
    auto entry = ledger->read(keylet::account(accountID));
    if (entry) entries.push_back(entry);
}
```

### Memory Efficiency

```cpp
// BAD: Creates large temporary structures
Json::Value getAllData(RPC::JsonContext& context)
{
    Json::Value allData(Json::arrayValue);

    for (auto i = 0; i < 1000000; ++i) {
        allData.append(expensiveQuery(i));  // 1M items in memory
    }

    return allData;
}

// GOOD: Stream or paginate large results
Json::Value getPagedData(RPC::JsonContext& context)
{
    unsigned int page = context.params.get("page", 0).asUInt();
    unsigned int pageSize = 1000;

    Json::Value result(Json::arrayValue);

    unsigned int start = page * pageSize;
    unsigned int end = start + pageSize;

    for (unsigned int i = start; i < end; ++i) {
        auto item = expensiveQuery(i);
        if (item) result.append(item);
    }

    return result;
}
```

### String Operations

```cpp
// BAD: Multiple string concatenations
std::string message = "Error: ";
message += errorType;
message += " - ";
message += errorDetails;
message += " at line ";
message += std::to_string(lineNumber);

// GOOD: Use string formatting or streams
std::stringstream ss;
ss << "Error: " << errorType << " - "
   << errorDetails << " at line " << lineNumber;
std::string message = ss.str();

// Or use modern C++ formatting
std::string message = fmt::format(
    "Error: {} - {} at line {}",
    errorType, errorDetails, lineNumber);
```

***

## Documentation Standards

### Header Documentation

```cpp
/**
 * Handler to retrieve account information and statistics.
 *
 * This handler provides detailed information about an account including
 * balance, sequence number, and various flags. The response includes
 * metadata about the ledger used for the query.
 *
 * @param context Contains request parameters, ledger access, and caller role
 * @return JSON response with account information or error details
 *
 * Required permissions: Role::USER
 * Required conditions: NEEDS_CURRENT_LEDGER
 *
 * Parameters:
 *   - account (required): Account address in base58 format
 *   - ledger_index (optional): Ledger index or "validated"/"current"
 *
 * Response fields:
 *   - status: "success" on success, "error" on failure
 *   - account: The account address
 *   - balance: XRP balance in drops
 *   - sequence: Transaction sequence number
 *   - ledger_index: Index of the ledger used
 *   - validated: True if ledger is validated
 *
 * Error codes:
 *   - rpcINVALID_PARAMS: Missing or invalid parameters
 *   - rpcACT_MALFORMED: Account address invalid
 *   - rpcACT_NOT_FOUND: Account not found in ledger
 *   - rpcNO_CURRENT: No current ledger available
 */
Json::Value doAccountInfo(RPC::JsonContext& context);
```

### Inline Comments

```cpp
Json::Value doTransfer(RPC::JsonContext& context)
{
    // Validate sender and receiver are different
    auto const sender = parseBase58<AccountID>(
        context.params["sender"].asString());
    auto const receiver = parseBase58<AccountID>(
        context.params["receiver"].asString());

    if (sender == receiver) {
        return rpcError(rpcINVALID_PARAMS,
            "Sender and receiver cannot be the same");
    }

    // Fetch the current validated ledger
    std::shared_ptr<ReadView const> ledger;
    auto const ledgerResult = RPC::lookupLedger(ledger, context);

    if (!ledger) {
        return ledgerResult;
    }

    // ... more logic
}
```

### Documentation Best Practices

* **Be concise**: Avoid verbose explanations
* **Be accurate**: Keep docs in sync with code
* **Be clear**: Use precise language
* **Be complete**: Document all parameters and return values
* **Example usage**: Show common use cases

***

## Maintenance Best Practices

### Version Your Handlers

When making breaking changes, implement versioning:

```cpp
// Original handler
Json::Value doMyCommand_v1(RPC::JsonContext& context)
{
    // Original implementation
}

// New version with breaking changes
Json::Value doMyCommand_v2(RPC::JsonContext& context)
{
    // New implementation
}

// Register both versions
{
    "my_command",
    {
        &doMyCommand_v1,
        Role::USER,
        RPC::NEEDS_CURRENT_LEDGER,
        1,  // Version 1
        1   // Only version 1
    }
}

{
    "my_command",
    {
        &doMyCommand_v2,
        Role::USER,
        RPC::NEEDS_CURRENT_LEDGER,
        2,  // Version 2+
        UINT_MAX
    }
}
```

### Add Deprecation Warnings

```cpp
Json::Value doOldHandler(RPC::JsonContext& context)
{
    // Log deprecation warning
    JLOG(context.app.journal("RPC"))
        << "Deprecated RPC command: old_command. "
        << "Use new_command instead.";

    // Still work but mark as deprecated
    Json::Value result = doNewHandler(context);
    result["deprecated"] = true;
    result["use_instead"] = "new_command";

    return result;
}
```

### Logging Standards

```cpp
// Use different severity levels appropriately

// Error - something went wrong
JLOG(context.app.journal("RPC"))
    << "Database error in account_info: " << errorMessage;

// Warning - unusual but acceptable condition
JLOG(context.app.journal("RPC"))
    << "Slow query detected: " << executionTime << "ms";

// Info - important events
JLOG(context.app.journal("RPC"))
    << "New handler registered: my_command";

// Debug - detailed diagnostic info
JLOG(context.app.journal("RPC"))
    << "Parsing account: " << accountString;
```

### Configuration Management

```cpp
// Don't hardcode limits - use configuration
struct HandlerConfig {
    static constexpr unsigned int MAX_PAGE_SIZE =
        Config::getInstance().get("rpc.max_page_size", 1000);

    static constexpr unsigned int CACHE_TTL_SECONDS =
        Config::getInstance().get("rpc.cache_ttl", 3600);

    static constexpr bool ENABLE_DETAILED_LOGGING =
        Config::getInstance().get("rpc.detailed_logging", false);
};

Json::Value doMyHandler(RPC::JsonContext& context)
{
    unsigned int pageSize = context.params.get("limit", 20).asUInt();

    if (pageSize > HandlerConfig::MAX_PAGE_SIZE) {
        pageSize = HandlerConfig::MAX_PAGE_SIZE;
    }

    // ...
}
```

### Testing for Regressions

```cpp
// Always write a test when you fix a bug
TEST_F(AccountInfoTest, FixedIssue12345_HandlesSpecialCharacters) {
    // This test documents the fix for issue #12345
    // It verifies that accounts with special characters work correctly

    Json::Value params;
    params[jss::account] = "rN7n7otQDd6FczFgLdlqtyMVrn3NnrcVXs";

    RPC::JsonContext context = createContext(params);
    Json::Value result = doAccountInfo(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    // Regression test ensures this doesn't break again
}
```

***

## Code Review Checklist

Before submitting a handler for review, verify:

### Functionality

* [ ] Handler works correctly for all test cases
* [ ] All parameters are validated
* [ ] All error paths return proper error codes
* [ ] Response format matches specification

### Security

* [ ] No hardcoded credentials or secrets
* [ ] Sensitive data is not exposed in responses
* [ ] Role/permission checks are implemented
* [ ] Input validation prevents injection attacks
* [ ] Resource limits are enforced

### Performance

* [ ] No obvious performance bottlenecks
* [ ] Ledger access is optimized
* [ ] Memory usage is reasonable
* [ ] Large result sets are paginated

### Maintainability

* [ ] Code follows naming conventions
* [ ] Functions are appropriately sized
* [ ] Complex logic is commented
* [ ] No duplicate code (DRY principle)

### Documentation

* [ ] Header comments explain purpose and parameters
* [ ] Complex sections have inline comments
* [ ] Error codes are documented
* [ ] Examples are provided

### Testing

* [ ] All code paths are covered by tests
* [ ] Error conditions are tested
* [ ] Different roles are tested
* [ ] Edge cases are considered

***

## Example: Well-Implemented Handler

```cpp
/**
 * Handler to retrieve paginated transaction history for an account.
 *
 * Returns recent transactions affecting the specified account, with
 * support for pagination via marker tokens.
 *
 * @param context Contains request parameters and ledger access
 * @return JSON response with transaction list or error
 *
 * Parameters:
 *   - account (required): Account in base58 format
 *   - ledger_index (optional): Ledger to query (default: validated)
 *   - limit (optional): Results per page, 1-1000 (default: 100)
 *   - marker (optional): Pagination marker from previous response
 *
 * Response includes:
 *   - transactions: Array of transaction objects
 *   - marker: Token for fetching next page (if more results exist)
 */
Json::Value doAccountTransactions(RPC::JsonContext& context)
{
    // Input validation
    if (!context.params.isMember(jss::account)) {
        return rpcError(rpcINVALID_PARAMS, "Missing 'account' field");
    }

    // Parse account
    auto const account = parseBase58<AccountID>(
        context.params[jss::account].asString());

    if (!account) {
        return rpcError(rpcACT_MALFORMED, "Invalid account address");
    }

    // Validate pagination parameters
    unsigned int pageSize = 100;
    if (context.params.isMember("limit")) {
        if (!context.params["limit"].isUInt()) {
            return rpcError(rpcINVALID_PARAMS,
                "'limit' must be a positive integer");
        }

        pageSize = context.params["limit"].asUInt();
        if (pageSize < 1 || pageSize > 1000) {
            return rpcError(rpcINVALID_PARAMS,
                "'limit' must be between 1 and 1000");
        }
    }

    // Get ledger
    std::shared_ptr<ReadView const> ledger;
    auto const ledgerResult = RPC::lookupLedger(ledger, context);

    if (!ledger) {
        return ledgerResult;
    }

    // Fetch transactions
    std::vector<Json::Value> transactions;
    std::string nextMarker;

    auto const ledgerSeq = ledger->info().seq;

    // Query transaction history (implementation details omitted)
    for (auto seq = ledgerSeq; seq > 0 && transactions.size() < pageSize; --seq) {
        auto const txLedger = context.ledgerMaster.getLedgerBySeq(seq);
        if (!txLedger) continue;

        // Get transactions from this ledger
        // Filter by account...
        // Build transaction objects...
    }

    // Build response
    Json::Value result;
    result[jss::status] = jss::success;
    result[jss::account] = to_string(*account);
    result[jss::ledger_index] = ledger->info().seq;
    result[jss::validated] = ledger->isImmutable();

    Json::Value txArray(Json::arrayValue);
    for (auto const& tx : transactions) {
        txArray.append(tx);
    }
    result["transactions"] = txArray;

    // Add marker if more results available
    if (!nextMarker.empty()) {
        result["marker"] = nextMarker;
    }

    return result;
}
```

***

### Conclusion

Best practices and patterns distill years of experience into actionable guidelines that elevate your handlers from working code to production-ready software. Consistent naming, thorough documentation, defensive programming, performance optimization, and security-first thinking create handlers that are maintainable, efficient, and safe. Following these standards ensures your contributions align with Rippled's codebase quality and can be confidently deployed in real-world environments.

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module07/best-practices-patterns.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
