← Back to All Guides
1
2
3
4
5
1
Architecture Overview

Real-time toll processing at 9,000+ TPS with vehicle lookup, account charging, and bill-by-mail for unknown vehicles. This demo showcases VoltDB's sub-millisecond stored procedures combined with VoltSP's streaming pipeline to process license plate scans, validate vehicle registrations, calculate tolls, and route unknown vehicles to an external billing stream.

Prerequisites

  • VoltDB 14.1+
  • VoltSP 1.7+
  • Java 21
  • Maven

Understanding the Data Flow

The Toll Collection system processes license plate scans in real-time, looking up vehicle information and charging accounts instantly.

  • VoltSP generates simulated license plate scans at ~9,000 TPS
  • ProcessPlate procedure validates plate against KNOWN_VEHICLES
  • Known vehicles have tolls charged to their accounts
  • Unknown vehicles are exported to bill-by-mail stream
Tip

The system processes each plate scan in under 1ms, including account lookup and toll calculation.

2
Schema Definition (DDL)

Reference Tables

Replicated tables hold static reference data that doesn't change frequently.

  • VEHICLE_TYPES stores toll multipliers per vehicle class
  • TOLL_LOCATIONS stores base fares for each toll plaza
  • These tables are replicated to all partitions
  • Enables fast local lookups without cross-partition queries
Tip

Replicated tables are ideal for small, frequently-read reference data.

Partitioned Tables

High-velocity tables are partitioned for parallel processing.

  • KNOWN_VEHICLES partitioned on plate_num for plate lookups
  • SCAN_HISTORY partitioned on plate_num with 1-hour TTL
  • ACCOUNTS partitioned on account_id for balance updates
  • TTL automatically removes old scan history
Tip

Partitioning on plate_num means all scans for the same plate go to the same partition.

Export Streams

Streams export data to external systems without persistence.

  • bill_by_mail_stream exports unknown vehicle scans
  • EXPORT TO TOPIC sends to Kafka/external consumers
  • WITH KEY ensures ordering by scan_id
  • Fire-and-forget writes with guaranteed delivery
Tip

Streams are perfect for sending events to downstream systems like billing or analytics.

Views & Procedures

Views provide real-time aggregations and procedures handle toll logic.

  • highest_grossing_locations aggregates toll revenue
  • location_scans counts scans per toll plaza
  • ProcessPlate registered with plate_num partitioning
  • PARAMETER 3 indicates which argument contains partition key
Tip

Materialized views update automatically as data is inserted.

SQL
-- VoltDB Schema for Toll Collection System
-- Processes license plate scans in real-time with account charging

-------------- REPLICATED TABLES ------------------------------------------------
-- Reference data that doesn't need partitioning

CREATE TABLE VEHICLE_TYPES (
  vehicle_type      SMALLINT           NOT NULL,
  vehicle_class     VARCHAR(20)        NOT NULL,
  toll_multip       DECIMAL            NOT NULL,
  PRIMARY KEY (vehicle_type)
);

CREATE TABLE TOLL_LOCATIONS (
  toll_loc_id       SMALLINT           NOT NULL,
  toll_loc          VARCHAR(64)        NOT NULL,
  toll_loc_status   TINYINT            NOT NULL,
  base_fare         DECIMAL            NOT NULL,
  latitude          DECIMAL            NOT NULL,
  longitude         DECIMAL            NOT NULL,
  PRIMARY KEY (toll_loc_id)
);

CREATE TABLE APPLICATION_PARAMETERS (
  parameter_name    VARCHAR(30) NOT NULL PRIMARY KEY,
  parameter_value   VARCHAR(40) NOT NULL
);

-------------- PARTITIONED TABLES -----------------------------------------------
-- Tables partitioned for parallel processing

CREATE TABLE KNOWN_VEHICLES (
  plate_num         VARCHAR(20)       NOT NULL,
  account_id        INTEGER           NOT NULL,
  vehicle_type      SMALLINT          NOT NULL,
  active            TINYINT           NOT NULL,
  exempt_status     TINYINT           NOT NULL,
  PRIMARY KEY (plate_num)
);
PARTITION TABLE KNOWN_VEHICLES ON COLUMN plate_num;

CREATE TABLE SCAN_HISTORY (
  scan_id           BIGINT            NOT NULL,
  scan_timestamp    TIMESTAMP         NOT NULL,
  plate_num         VARCHAR(20)       NOT NULL,
  account_id        INTEGER,
  toll_loc          VARCHAR(64)       NOT NULL,
  toll_lane_num     VARCHAR(2)        NOT NULL,
  toll_amount       DECIMAL           NOT NULL,
  toll_reason       VARCHAR(200),
  scan_fee_amount   DECIMAL,
  total_amount      DECIMAL           NOT NULL,
  PRIMARY KEY (plate_num, scan_id)
) USING TTL 3600 SECONDS ON COLUMN scan_timestamp;
PARTITION TABLE SCAN_HISTORY ON COLUMN plate_num;

CREATE TABLE ACCOUNTS (
  account_id        INTEGER           NOT NULL,
  account_status    TINYINT           NOT NULL,
  auto_topup        TINYINT           NOT NULL,
  balance           DECIMAL           NOT NULL,
  PRIMARY KEY (account_id)
);
PARTITION TABLE ACCOUNTS ON COLUMN account_id;

-------------- STREAMS ----------------------------------------------------------
-- Output streams for external systems

CREATE STREAM bill_by_mail_stream
PARTITION ON COLUMN plate_num
EXPORT TO TOPIC bill_by_mail_topic
WITH KEY (scan_id) (
  scan_id           BIGINT            NOT NULL,
  scan_timestamp    TIMESTAMP         NOT NULL,
  plate_num         VARCHAR(20)       NOT NULL,
  toll_loc          VARCHAR(20)       NOT NULL,
  toll_lane_num     VARCHAR(2)        NOT NULL,
  toll_amount       DECIMAL           NOT NULL,
  toll_reason       VARCHAR(20),
  scan_fee_amount   DECIMAL,
  tx_fee_amount     DECIMAL,
  total_amount      DECIMAL           NOT NULL
);

-------------- VIEWS ----------------------------------------------------------

CREATE VIEW highest_grossing_locations (toll_loc, total_toll_amount) AS
SELECT toll_loc, SUM(toll_amount) AS total_toll_amount
FROM SCAN_HISTORY GROUP BY toll_loc;

CREATE VIEW location_scans (toll_loc, total_count) AS
SELECT toll_loc, COUNT(*) AS total_count
FROM SCAN_HISTORY GROUP BY toll_loc;

-------------- STORED PROCEDURES -----------------------------------------------

CREATE PROCEDURE PARTITION ON TABLE scan_history COLUMN plate_num PARAMETER 3
FROM CLASS com.voltdb.tollcollect.procedures.ProcessPlate;
3
Java Stored Procedure

Package & Imports

Define the package and import necessary VoltDB classes.

  • Package matches your project structure
  • Import VoltDB core classes (SQLStmt, VoltProcedure, VoltTable)
  • BigDecimal for precise toll calculations
  • VoltType for accessing column types
Tip

VoltDB procedures must extend VoltProcedure base class.

SQL Statements

Pre-compiled SQL statements for lookups and inserts.

  • getTollInfo looks up base fare for toll location
  • getVehicleMultiplier gets multiplier for vehicle class
  • checkVehicle validates plate against KNOWN_VEHICLES
  • SQLStmt objects are compiled once at load time
Tip

Pre-compiled statements eliminate SQL parsing overhead at runtime.

Run Method - Lookup Phase

The entry point fetches toll location and vehicle information.

  • scanTimestamp, location, lane, plateNum, vehicleClass are inputs
  • getUniqueId() generates cluster-wide unique scan ID
  • Query base fare and vehicle multiplier
  • Calculate toll amount: baseToll x vehicleMultiplier
Tip

Each voltExecuteSQL() is a separate database round-trip within the transaction.

Run Method - Charge Phase

Check vehicle registration and process the toll charge.

  • If plate is in KNOWN_VEHICLES, charge to account
  • Exempt vehicles pay $0
  • Unknown vehicles get $2 scan fee + bill-by-mail export
  • Insert scan record into SCAN_HISTORY
Tip

The bill-by-mail stream enables offline billing for vehicles without transponders.

Java
/*
 * Copyright (C) 2025 Volt Active Data Inc.
 * ProcessPlate - Stored procedure for toll collection
 */
package com.voltdb.tollcollect.procedures;

import org.voltdb.SQLStmt;
import org.voltdb.VoltProcedure;
import org.voltdb.VoltTable;
import org.voltdb.VoltTableRow;
import org.voltdb.VoltType;
import java.math.BigDecimal;

public class ProcessPlate extends VoltProcedure {

    // SQL statements for lookups
    public final SQLStmt getTollInfo = new SQLStmt(
        "SELECT base_fare FROM TOLL_LOCATIONS WHERE toll_loc = ? AND toll_loc_status = 1;"
    );

    public final SQLStmt getVehicleMultiplier = new SQLStmt(
        "SELECT toll_multip FROM VEHICLE_TYPES WHERE vehicle_class = ?;"
    );

    public final SQLStmt checkVehicle = new SQLStmt(
        "SELECT account_id, exempt_status, vehicle_type FROM KNOWN_VEHICLES " +
        "WHERE plate_num = ? AND active = 1;"
    );

    // SQL statements for inserts
    public final SQLStmt exportBillByMail = new SQLStmt(
        "INSERT INTO bill_by_mail_stream VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
    );

    public final SQLStmt insertScanHistory = new SQLStmt(
        "INSERT INTO SCAN_HISTORY VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
    );

    public long run(
            long scanTimestamp,
            String location,
            String lane,
            String plateNum,
            String vehicleClass) throws VoltAbortException {

        final long scanId = getUniqueId();

        // Initialize toll calculation variables
        BigDecimal baseToll;
        BigDecimal vehicleMultiplier;
        BigDecimal scanFeeAmount = new BigDecimal(0);
        BigDecimal tollAmount;
        BigDecimal totalAmount;
        String tollReason;
        int accountId = 0;
        byte exemptStatus;

        // Get base toll for location
        voltQueueSQL(getTollInfo, location);
        VoltTable[] tollResults = voltExecuteSQL();
        if (tollResults[0].getRowCount() == 0) {
            throw new VoltAbortException("Invalid toll location");
        }

        // Get vehicle type multiplier
        voltQueueSQL(getVehicleMultiplier, vehicleClass);
        VoltTable[] vehicleResults = voltExecuteSQL();
        if (vehicleResults[0].getRowCount() == 0) {
            throw new VoltAbortException("Invalid vehicle class");
        }

        baseToll = tollResults[0].fetchRow(0).getDecimalAsBigDecimal("base_fare");
        vehicleMultiplier = vehicleResults[0].fetchRow(0).getDecimalAsBigDecimal("toll_multip");
        tollAmount = baseToll.multiply(vehicleMultiplier);

        // Check if vehicle is known
        voltQueueSQL(checkVehicle, plateNum);
        VoltTable[] vehicleCheckResults = voltExecuteSQL();

        if (vehicleCheckResults[0].getRowCount() > 0) {
            VoltTableRow vehicleRow = vehicleCheckResults[0].fetchRow(0);
            accountId = (int) vehicleRow.getLong("account_id");
            exemptStatus = (byte) vehicleRow.get("exempt_status", VoltType.TINYINT);

            if (exemptStatus == 1) {
                tollAmount = BigDecimal.ZERO;
                totalAmount = BigDecimal.ZERO;
                tollReason = "EXEMPT";
            } else {
                totalAmount = tollAmount;
                tollReason = "STANDARD TOLL (" + vehicleClass + ")";
            }
        } else {
            // Unknown vehicle - send to bill-by-mail
            scanFeeAmount = new BigDecimal(2);
            totalAmount = tollAmount.add(scanFeeAmount);
            tollReason = "UNKNOWN_VEHICLE";

            voltQueueSQL(exportBillByMail,
                scanId, new java.util.Date(scanTimestamp), plateNum, location, lane,
                tollAmount, tollReason, scanFeeAmount, null, tollAmount);
            voltExecuteSQL();
        }

        // Insert into scan history
        voltQueueSQL(insertScanHistory,
            scanId, new java.util.Date(scanTimestamp), plateNum, accountId,
            location, lane, tollAmount, tollReason, scanFeeAmount, totalAmount);

        voltExecuteSQL(true);
        return 0;
    }
}
4
VoltSP Pipeline (Java)

Pipeline Class

VoltSP pipeline that generates and processes toll events.

  • Implements VoltPipeline interface
  • define() method configures the stream
  • Reads tps and voltdb.server from config.yaml
  • No Kafka needed - generates data internally
Tip

Java pipelines offer more flexibility than YAML for complex processing.

Source Configuration

Generate plate records at the configured transactions per second.

  • Sources.generateAtRate() creates synthetic load
  • PlateRecordGenerator produces realistic plate scans
  • Configurable TPS for load testing
  • Simulates multiple toll plazas and vehicle types
Tip

This pipeline can generate 9,000+ TPS for realistic load testing.

Transform & Sink

Transform records to procedure arguments and call VoltDB.

  • processWith() maps PlateRecord to VoltProcedureRequest
  • Arguments match ProcessPlate signature
  • VoltProcedureSinkConfigBuilder sends requests to VoltDB
  • withVoltClientResourceName() links to the configured resource
Tip

The transform ensures arguments match the stored procedure parameter order.

Java
/*
 * VoltSP Java Pipeline for Toll Collection
 * Generates simulated license plate scans at configurable TPS
 */
package com.voltdb.tollcollect.pipeline;

import org.voltdb.stream.api.ExecutionContext;
import org.voltdb.stream.api.Sources;
import org.voltdb.stream.api.pipeline.VoltPipeline;
import org.voltdb.stream.api.pipeline.VoltStreamBuilder;
import org.voltdb.stream.plugin.volt.api.VoltDBResourceConfigBuilder;
import org.voltdb.stream.plugin.volt.api.VoltProcedureRequest;
import org.voltdb.stream.plugin.volt.api.VoltProcedureSinkConfigBuilder;

public class TollCollectStream implements VoltPipeline {

    @Override
    public void define(VoltStreamBuilder stream) {
        // Get configuration values
        ExecutionContext.ConfigurationContext config =
            stream.getExecutionContext().configurator();
        int tps = config.findByPath("tps").asInt();

        PlateRecordGenerator generator = new PlateRecordGenerator();

        stream
            .withName("Toll Collector Data Stream")
            .configureResource("voltdb", VoltDBResourceConfigBuilder.class)
            // Source: Generate plate records at specified TPS
            .consumeFromSource(
                Sources.generateAtRate(
                    tps,
                    generator::generatePlateRecord
                )
            )
            // Transform: Convert PlateRecord to VoltDB procedure call
            .processWith(
                record -> new VoltProcedureRequest("ProcessPlate",
                    new Object[]{
                        record.scanTimestamp(),
                        record.location(),
                        record.lane(),
                        record.plateNum(),
                        record.vehicleClass()
                    })
            )
            // Sink: Send procedure calls to VoltDB
            .terminateWithSink(
                VoltProcedureSinkConfigBuilder.builder()
                    .withVoltClientResourceName("voltdb")
            );
    }
}

// config.yaml example:
// className: com.voltdb.tollcollect.pipeline.TollCollectStream
// tps: 9000
5
See It Working

Live Toll Collection Dashboard

Watch real-time toll processing with live queries against VoltDB.

  • Click "Push DDL" to create schema and load reference data
  • Click "Start Pipeline" to begin VoltSP which generates toll events at ~9,000 TPS
  • The Query Results tab shows the last 10 toll scans processed
  • The VoltSP Logs tab shows real-time pipeline output
Tip

VoltSP generates simulated license plate scans internally - no external traffic generator needed!