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 endendFeatures
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) endendArchitecture
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)