Back to Work
Library Open Source

Conduit

Ruby USSD engine with state machine flows, session management, and Redis pub/sub. Powers interactive mobile menus for fintech applications.

The Problem

USSD development is painful. Sessions are stateless but flows are stateful. Telco APIs vary wildly. Most teams end up with spaghetti code—giant switch statements, scattered session logic, no structure. Testing is an afterthought.

The Solution

A structured USSD engine that brings sanity to interactive mobile menus.

class LoanFlow < Conduit::Flow
initial_state :welcome
state :welcome do
on_enter do |session|
session.respond "Welcome to QuickLoans\n1. Apply for loan\n2. Check balance\n3. Repay"
end
on_input "1" => :loan_amount
on_input "2" => :check_balance
on_input "3" => :repayment
end
state :loan_amount do
on_enter do |session|
session.respond "Enter loan amount (1000 - 50000):"
end
on_input do |session, input|
amount = input.to_i
if amount.between?(1000, 50000)
session.data[:amount] = amount
transition_to :confirm_loan
else
session.respond "Invalid amount. Enter between 1000 - 50000:"
stay
end
end
end
state :confirm_loan do
on_enter do |session|
amount = session.data[:amount]
fee = (amount * 0.05).round
session.respond "Loan: #{amount} KES\nFee: #{fee} KES\n\n1. Confirm\n2. Cancel"
end
on_input "1" => :process_loan
on_input "2" => :welcome
end
end

Features

State Machine DSL

  • Declarative state definitions
  • Input routing with pattern matching
  • Guards and conditional transitions
  • Timeout handling per state

Session Management

  • Redis-backed session storage
  • Automatic expiry (configurable TTL)
  • Session data persistence across requests
  • Phone number indexed sessions

Input Handling

  • Input validation and sanitization
  • Sensitive input obfuscation (PINs, OTPs)
  • Timeout detection
  • Cancel/back navigation

Telco Abstraction

  • Adapter pattern for different providers
  • Africa’s Talking integration
  • Safaricom USSD gateway support
  • Request/response normalization

Testing Support

  • Session simulator for flow testing
  • State transition assertions
  • Input sequence helpers
RSpec.describe LoanFlow do
it "completes loan application" do
session = simulate_flow(LoanFlow)
session.input("1") # Apply for loan
expect(session.state).to eq(:loan_amount)
session.input("5000")
expect(session.state).to eq(:confirm_loan)
expect(session.data[:amount]).to eq(5000)
end
end

Architecture

Flows are classes, states are methods. Each request hits the engine, loads session from Redis, executes the current state handler, and persists updated session. Clean separation between flow logic and transport layer.

Background jobs handle async operations—credit checks, disbursements—while the USSD session shows “Processing…” with automatic polling.

Production Stats

Conduit powers USSD flows across multiple platforms:

  • 100K+ sessions per month across all deployments
  • 25M+ KES disbursed via USSD-initiated transactions
  • 40+ state handlers in our largest flow (EWA platform)
  • Sub-100ms response times for menu navigation

Language: Ruby

Dependencies: Redis, Faraday

Compatibility: Ruby 3.0+, Rails 6+ (optional)

License: MIT

Used in: Asgard EWA Platform, USSD Loan System, Hive Payroll (leave requests)