| name | ruby-to-rust |
| description | Use when migrating Ruby codebases to Rust — covers dynamic-to-static typing, blocks/closures to Fn traits, metaprogramming to macros/generics, Rails to Axum/Actix, ActiveRecord to Diesel/sqlx, GIL to true parallelism, garbage collection to ownership, Bundler to Cargo, and incremental migration via sidecar services or FFI. Includes canonical code patterns, common mistakes, and reference implementations. |
| updated | 2026-07-30T00:00:00.000Z |
Ruby to Rust Migration
Architecture Mapping
Ruby's MRI/YARV interpreter (with generational GC, Global VM Lock, and object-oriented-everything paradigm) maps to Rust's AOT-compiled native binary with no runtime overhead. Where Ruby encourages "duck typing" and runtime metaprogramming (method_missing, define_method, class_eval), Rust requires compile-time type resolution with trait-based polymorphism. Ruby's GIL (Global VM Lock) is replaced by Rust's Send + Sync traits enabling true multi-threaded parallelism. A Rails application served by Puma becomes a single Rust binary served by Axum or Actix-Web. Bundler's Gemfile becomes Cargo.toml with lockfile-based reproducible builds.
Ruby's "developer happiness" philosophy maps to Rust's "compiler-as-teacher" approach — both prioritize ergonomics, but Rust enforces correctness at compile time. The critical paradigm shift: Ruby blocks, Procs, and Lambdas become Rust's closure traits (Fn, FnMut, FnOnce); Ruby's include/extend mixins become Rust's trait composition; and Ruby's exception-driven error handling becomes Result<T, E> with the ? operator.
| Ruby Concept | Rust Equivalent | Notes |
|---|
| MRI/YARV interpreter | rustc + LLVM (AOT) | No interpreter, no warmup |
| GC (generational, compaction) | Ownership + RAII | Deterministic, zero pause |
| GIL (Global VM Lock) | Send + Sync traits | True multi-threaded parallelism |
class / module | struct + impl + trait | Data + behavior separated |
Mixin (include / extend) | Trait + default methods | Composition over inheritance |
method_missing | Trait dispatch + generics | Compile-time, not runtime |
define_method (dynamic) | Proc macros / code generation | Compile-time metaprogramming |
attr_accessor | pub field or getter/setter | Explicit access |
Block / yield | FnMut closure + .call() or for | Explicit closure type |
| Proc / Lambda | Box<dyn Fn()> / fn() pointer | Typed callables |
&:method (symbol-to-proc) | Method reference T::method | Compile-time resolution |
unless / if modifier | if !cond { } / regular if | No postfix conditionals |
case/when | match expression | Exhaustive, expression-oriented |
rescue / ensure | Result<T, E> + ? / Drop | Errors as values |
|
Type System Mapping
| Ruby Type | Rust Type | Notes |
|---|
Integer (bigint) | i64 / i128 / num_bigint::BigInt | Ruby Integer is arbitrary-precision |
Float | f64 | IEEE 754 double |
Rational | num_rational::Rational64 | Fractional numbers |
Complex | num_complex::Complex<f64> | Complex numbers |
String | String / &str | Ruby strings are mutable byte sequences |
Symbol | &'static str / custom enum | Interned, immutable |
Array | Vec<T> | Ordered, indexed |
Hash | HashMap<K,V> / BTreeMap<K,V> | Key-value storage |
Set | HashSet<T> / BTreeSet<T> | Unique elements |
Range | Range<T> / RangeInclusive<T> | Start..end |
true / false | bool | Boolean values |
nil | Option::None | Absence of value |
Proc (callable) | Box<dyn Fn(A) -> R> / fn(A) -> R | Typed callable |
Time / DateTime | chrono::DateTime<Utc> | Timezone-aware |
Date | chrono::NaiveDate |
Memory & Ownership Model
Ruby's generational GC (with incremental marking and compaction) means objects live as long as they are reachable. Rust's ownership system replaces the GC entirely. This is the hardest mental shift for Ruby developers — there is no GC.start, no ObjectSpace, and no finalizer.
| Ruby Pattern | Rust Translation |
|---|
| Pass object, mutate in place | &mut T (exclusive borrow) |
| Pass object, read only | &T (shared borrow) |
| GC cleans up unreachable | Drop trait, scope-based |
ObjectSpace.define_finalizer | impl Drop for T |
WeakRef | Weak<T> (from Rc/Arc) |
dup / clone | .clone() (explicit, Clone trait) |
freeze | let binding (immutable by default) |
| Thread-local variables | thread_local! macro |
$SAFE / taint levels | Not applicable — memory safety at compile time |
The GC-Free Mindset
class OrderService
def initialize(db, payment_gateway)
@db = db
@payment_gateway = payment_gateway
end
def place_order(request)
total = request.items.sum(&:price)
charge = @payment_gateway.charge(total, request.card_token)
@db.orders.create(request.to_h.merge(charge_id: charge.id))
end
end
pub struct OrderService<P: PaymentGateway> {
db: PgPool,
payment: P,
}
impl<P: PaymentGateway> OrderService<P> {
pub async fn place_order(&self, request: &OrderRequest) -> Result<Order, OrderError> {
let total: f64 = request.items.iter().map(|i| i.price).sum();
let charge = self.payment.charge(total, &request.card_token).await?;
let order = sqlx::query_as::<_, Order>("INSERT INTO orders (...) VALUES (...) RETURNING *")
.bind(&request.customer_id)
.bind(total)
.bind(&charge.id)
.fetch_one(&self.db)
.await?;
Ok(order)
}
}
Concurrency / Async Translation
Ruby has multiple concurrency models: threads (GIL-limited for CPU, usable for I/O), fibers (cooperative), Ractors (experimental, isolated), and async gems (async, eventmachine). Rust provides true multi-threaded parallelism via tokio for async I/O and rayon for CPU-bound work.
| Ruby | Rust / Tokio |
|---|
Thread.new { } | `std::thread::spawn(move |
Thread.new { }.join | handle.join().unwrap() |
Fiber.new { }.resume | async fn + .await (stackless coroutines) |
Async { } (async gem) | tokio::spawn(async { }) |
Async::Barrier | tokio::sync::Barrier |
sleep(n) | tokio::time::sleep(Duration::from_secs(n)).await |
Timeout.timeout(n) { } | tokio::time::timeout(dur, future) |
Queue (stdlib, thread-safe) | std::sync::mpsc::channel |
Concurrent::Future (concurrent-ruby) | tokio::task::JoinHandle<T> |
Concurrent::Promise | futures::future::Map combinators |
| Ractor | std::thread::spawn + mpsc channel |
Mutex (stdlib) | std::sync::Mutex<T> |
Monitor / MonitorMixin | std::sync::Mutex<T> + Condvar |
ConditionVariable | std::sync::Condvar |
Concurrent::Array / Concurrent::Hash | Arc<Mutex<Vec<T>>> / Arc<Mutex<HashMap<K,V>>> |
EventMachine | tokio runtime (work-stealing M:N scheduler) |
| Sidekiq / background jobs | tokio::spawn + Redis / apalis crate |
async.rb → tokio
require 'async'
Async do
user = Async { fetch_user(id) }
orders = Async { fetch_orders(id) }
Dashboard.new(user.wait, orders.wait)
end
async fn load_dashboard(id: &str) -> Result<Dashboard, AppError> {
let (user, orders) = tokio::join!(
fetch_user(id),
fetch_orders(id),
);
Ok(Dashboard::new(user?, orders?))
}
Build System & Dependencies
| Ruby Tool | Rust Equivalent |
|---|
Gemfile + Gemfile.lock | Cargo.toml + Cargo.lock |
bundle install | cargo build |
bundle exec | cargo run |
| Bundler / RubyGems | Cargo / crates.io |
rbenv / rvm (Ruby versions) | rustup (Rust toolchain) |
rake (task runner) | cargo subcommands / just / xtask |
| RuboCop / Standard | cargo clippy / cargo fmt |
| RSpec / Minitest | #[test] + cargo test |
| SimpleCov | cargo tarpaulin / cargo-llvm-cov |
pry / irb (REPL) | evcxr (Rust REPL) / cargo script |
| Spring / Bootsnap (preloader) | Not needed — AOT compilation |
| Sorbet / RBS (type checking) | Built-in static type system |
Cargo.toml for a migrated Rails API service:
[package]
name = "web-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
thiserror = "2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
dotenvy = "0.15"
reqwest = { version = "0.12", features = ["json"] }
jsonwebtoken =
=
Framework Mapping: Rails → Axum/Actix
| Rails Component | Rust Equivalent | Notes |
|---|
config/routes.rb | Router::route(path, method(handler)) | Route definitions |
ApplicationController | Shared middleware / Extension layers | Base behavior |
before_action callback | Middleware layer / tower::ServiceBuilder | Pre-handler logic |
params (ActionController) | Json<T> / Query<T> / Path<T> extractor | Typed extraction |
render json: | Json(response) | Serde-based serialization |
| Active Record model | sqlx::FromRow / Diesel schema | ORM mapping |
| Active Record query | sqlx::query_as / diesel::dsl | Compile-time checked |
| Active Record migration | sqlx migrate / diesel migration | Versioned SQL |
| Active Record validation | validator crate / constructor validation | Typed validation |
| Active Job / Sidekiq | tokio::spawn + Redis | Background processing |
| Action Cable (WebSocket) | axum::extract::ws | Built-in WebSocket |
| Action Mailer | lettre crate / aws-sdk-ses | Email delivery |
| Active Storage | aws-sdk-s3 / object_store crate | File storage |
| Devise (auth) | axum-login / custom JWT middleware | Authentication |
| Pundit / CanCanCan | Custom guard trait or middleware | Authorization |
| Rails Console | evcxr REPL |
Standard Library & Ecosystem Mapping
Core Extensions
| Ruby | Rust |
|---|
| `[1,2,3].map { | x |
[1,2,3].select(&:even?) | `vec.iter().filter( |
| `[1,2,3].reject { | x |
[1,2,3].reduce(0, &:+) | vec.iter().sum() |
| `[1,2,3].each { | x |
| `[1,2,3].flat_map { | x |
| `[1,2,3].find { | x |
[1,2,3].any?(&:even?) | `vec.iter().any( |
| `[1,2,3].all? { | x |
[1,2,3].include?(2) | vec.contains(&2) |
[1,2,3].first(2) / .last(2) | &vec[..2] / vec.iter().rev().take(2) |
(1..10).step(2) | (1..=10).step_by(2) |
"hello".upcase / .downcase | "hello".to_uppercase() / .to_lowercase() |
"hello".reverse | "hello".chars().rev().collect::<String>() |
"hello".gsub(/l/, 'w') | regex.replace_all("hello", "w") |
"hello".start_with?("he") | "hello".starts_with("he") |
"a,b,c".split(",") | "a,b,c".split(',') returns iterator |
["a","b"].join(",") | ["a","b"].join(",") or itertools |
" trim ".strip | " trim ".trim() |
"%05d" % 42 / sprintf | format!("{n:05}") |
hash = { a: 1, b: 2 } | let mut map = HashMap::new(); map.insert("a", 1); |
Canonical Patterns
1. Class → Struct + impl
class OrderNotifier
def initialize(email_service, template_engine)
@email = email_service
@templates = template_engine
end
def notify_shipped(order)
body = @templates.render('shipped', order: order)
@email.send(to: order.customer_email, subject: "Shipped!", body: body)
end
end
pub struct OrderNotifier<E: EmailService, T: TemplateEngine> {
email: E,
templates: T,
}
impl<E: EmailService, T: TemplateEngine> OrderNotifier<E, T> {
pub fn new(email: E, templates: T) -> Self {
Self { email, templates }
}
pub async fn notify_shipped(&self, order: &Order) -> Result<(), AppError> {
let body = self.templates.render("shipped", &order.into_context())?;
self.email
.send(&order.customer_email, "Shipped!", &body)
.await?;
Ok(())
}
}
2. Block → Closure / Iterator
def active_user_names(users)
users
.select { |u| u.active? }
.map(&:name)
.sort
end
fn active_user_names(users: &[User]) -> Vec<String> {
let mut names: Vec<_> = users
.iter()
.filter(|u| u.active)
.map(|u| u.name.clone())
.collect();
names.sort();
names
}
3. Exception → Result
def transfer(from, to, amount)
raise ArgumentError, "invalid amount" if amount <= 0
raise InsufficientFundsError unless from.balance >= amount
from.withdraw(amount)
to.deposit(amount)
rescue InsufficientFundsError => e
log_error(e)
raise
ensure
audit_log.record(from, to, amount)
end
fn transfer(from: &mut Account, to: &mut Account, amount: f64) -> Result<(), TransferError> {
if amount <= 0.0 { return Err(TransferError::InvalidAmount); }
if from.balance < amount { return Err(TransferError::InsufficientFunds); }
let _guard = AuditGuard::new(from.id, to.id, amount);
from.withdraw(amount)?;
to.deposit(amount)?;
Ok(())
}
4. DSL → Builder Pattern
Rails.application.configure do
config.cache_classes = true
config.log_level = :info
end
use typed_builder::TypedBuilder;
#[derive(TypedBuilder)]
pub struct AppConfig {
#[builder(default = true)]
cache_classes: bool,
#[builder(default = "info")]
log_level: String,
}
let config = AppConfig::builder()
.cache_classes(true)
.log_level("info".into())
.build();
5. Mixin → Trait
module Timestampable
def touch
@updated_at = Time.now
end
end
class Post
include Timestampable
end
pub trait Timestampable {
fn touch(&mut self) {
}
}
impl Timestampable for Post {
}
6. Rails Controller → Axum Handler
class OrdersController < ApplicationController
before_action :authenticate_user!
before_action :set_order, only: [:show]
def create
@order = current_user.orders.build(order_params)
if @order.save
OrderMailer.confirmation(@order).deliver_later
render json: @order, status: :created
else
render json: { errors: @order.errors }, status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(:product_id, :quantity, :address)
end
def set_order
@order = current_user.orders.find(params[:id])
end
end
use axum::{Router, routing::{get, post}, extract::{State, Path, Json}, middleware};
use tower::ServiceBuilder;
async fn create_order(
State(state): State<Arc<AppState>>,
Extension(user): Extension<AuthUser>,
Json(req): Json<CreateOrderRequest>,
) -> Result<(StatusCode, Json<OrderResponse>), AppError> {
let mut tx = state.db.begin().await?;
let order = sqlx::query_as::<_, Order>(
"INSERT INTO orders (...) VALUES (...) RETURNING *"
)
.bind(&user.id)
.bind(&req.product_id)
.bind(req.quantity)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
tokio::spawn(send_confirmation_email(state.email.clone(), order.clone()));
Ok((StatusCode::CREATED, Json(order.into())))
}
let app = Router::new()
.(, (create_order).(list_orders))
.(ServiceBuilder::()
.(middleware::(auth_middleware))
.(tower_http::trace::TraceLayer::()));
7. Sidekiq Worker → Tokio Background Task
class OrderFulfillmentWorker
include Sidekiq::Worker
sidekiq_options queue: :high, retry: 3
def perform(order_id)
order = Order.find(order_id)
inventory = InventoryService.new
inventory.reserve(order.line_items)
shipping = ShippingService.new
shipping.create_label(order)
order.update!(status: 'fulfilled')
rescue InventoryError => e
order.update!(status: 'backordered')
OrderMailer.backordered(order).deliver_later
raise
end
end
OrderFulfillmentWorker.perform_async(order.id)
use redis::AsyncCommands;
use tokio::sync::Semaphore;
async fn fulfill_order_worker(
db: PgPool,
redis: redis::aio::MultiplexedConnection,
concurrency: Arc<Semaphore>,
) -> Result<(), AppError> {
loop {
let _permit = concurrency.acquire().await.unwrap();
let (_, order_id): (String, i64) = redis
.clone()
.brpop("queue:order_fulfillment", 0.0)
.await?;
let db = db.clone();
tokio::spawn(async move {
match process_fulfillment(&db, order_id).await {
Ok(()) => tracing::info!(order_id, "fulfilled"),
Err(e) => {
tracing::error!(order_id, error = %e, "fulfillment failed");
}
}
});
}
}
async fn process_fulfillment(db: &PgPool, order_id: ) <(), AppError> {
= db.().?;
= sqlx::query_as::<_, Order>()
.(order_id)
.(& *tx)
.?;
(& tx, &order).?;
(& tx, &order).?;
sqlx::()
.(order_id)
.(& *tx)
.?;
tx.().?;
(())
}
..num_workers {
tokio::((db.(), redis.(), sema.()));
}
8. ActiveRecord Callbacks → Explicit Middleware / Constructor Validation
class Order < ApplicationRecord
before_validation :generate_order_number, on: :create
before_save :calculate_total
after_commit :notify_customer, on: :create
validates :customer_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :quantity, numericality: { greater_than: 0 }
private
def generate_order_number
self.order_number = "ORD-#{Time.now.strftime('%Y%m%d')}-#{SecureRandom.hex(4)}"
end
def calculate_total
self.total = line_items.sum { |li| li.price * li.quantity }
end
def notify_customer
OrderMailer.confirmation(self).deliver_later
end
end
#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)]
pub struct Order {
pub id: i64,
pub order_number: String,
pub customer_email: String,
pub quantity: i32,
pub total: f64,
pub status: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct CreateOrderRequest {
pub customer_email: String,
pub quantity: i32,
pub line_items: Vec<LineItemInput>,
}
impl CreateOrderRequest {
pub fn validate(self) -> Result<ValidatedOrder, ValidationError> {
if self.customer_email.is_empty() || !self.customer_email.contains('@') {
return Err(ValidationError("invalid email".into()));
}
if .quantity <= {
((.()));
}
(ValidatedOrder {
order_number: ::(),
customer_email: .customer_email,
quantity: .quantity,
total: .line_items.().(|li| li.price * li.quantity).(),
})
}
() {
(,
chrono::Utc::().format(),
&uuid::Uuid::().()[..])
}
}
(
db: &PgPool,
email: &EmailService,
req: CreateOrderRequest,
) <Order, AppError> {
= req.()?;
= sqlx::query_as::<_, Order>(
)
.(&validated.order_number)
.(&validated.customer_email)
.(validated.quantity)
.(validated.total)
.(db)
.?;
tokio::({
= email.();
= order.();
{
= email.(&order).;
}
});
(order)
}
FFI & Incremental Migration
| Strategy | Tool | When |
|---|
| Sidecar binary | JSON/stdin-stdout or HTTP localhost | Batch processing, workers |
| FFI via C ABI | extern "C" + FFI gem | Performance-critical functions |
| HTTP/gRPC extraction | Axum web service + reverse proxy | API boundary migration |
| rubsys / magnus | magnus crate — Ruby extensions in Rust | Embed Rust in Ruby gems |
| Helix / Rutie | Rust-native Ruby extensions | Replace C extensions |
| Shared database | Both read/write same DB | Transitional phase |
Magnus: Rust Extensions for Ruby (Recommended)
require 'my_rust_lib'
result = MyRustLib.heavy_computation(large_dataset)
use magnus::{function, prelude::*, Error, Ruby};
fn heavy_computation(data: Vec<i64>) -> Vec<i64> {
data.into_iter().map(|x| x * 2).collect()
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("MyRustLib")?;
module.define_singleton_method("heavy_computation", function!(heavy_computation, 1))?;
Ok(())
}
Migration Order
- Schema & types: Define shared JSON/Protobuf schema; both Ruby and Rust consume
- Hot-path functions: Replace CPU-intensive Ruby methods with magnus-extended Rust
- Background jobs: Replace Sidekiq workers with Rust binaries reading the same Redis
- Read endpoints: Build Rust service reading the same database; route via nginx
- Write endpoints: Migrate mutation handlers; maintain transactional consistency
- Full cutover: Remove Ruby runtime; keep magnus bridges for scripting
Common Mistakes
Mistake 1: unwrap() Everywhere (Nil-Check Mindset)
let user = db.find(id).unwrap();
let name = user.name.unwrap();
let user = db.find(id)?.ok_or(AppError::NotFound)?;
let name = user.name.unwrap_or("anonymous".into());
Mistake 2: Overusing clone() (GC Mindset)
items.each { |item| cache.set(item.id, item.dup) }
for item in &items {
cache.set(item.id.clone(), item.clone());
}
for item in &items {
cache.set(&item.id, item);
}
Mistake 3: Trying to Recreate method_missing
enum Action { Create, Update, Delete }
impl Handler {
fn handle(&self, action: Action) -> Result<(), AppError> {
match action {
Action::Create => self.create()?,
Action::Update => self.update()?,
Action::Delete => self.delete()?,
}
}
}
Mistake 4: Mutable String Confusion
name = "Alice"
name << " Smith"
name.upcase!
let mut name = String::from("Alice");
name.push_str(" Smith");
let upper = name.to_uppercase();
Reference Implementations
| Project | Description | Pattern |
|---|
| YJIT (CRuby JIT) | Ruby JIT compiler; C → Rust core | Incremental replacement in MRI |
| Parser (Prism) | Ruby parser; C+Ruby → Rust | Performance-critical library rewrite |
| Artichoke | Ruby interpreter in Rust | Full runtime reimplementation |
| magnus | Ruby C extension replacement | FFI crate for Ruby↔Rust |
| Meilisearch | Ruby SDK + Rust core | Shared wire protocol |
| Sorbet (partial) | Ruby type checker | Static analysis in Rust |
Cross-Reference
- python-to-rust: Shared dynamic-to-static migration patterns; Django/Rails framework parallels
- php-to-rust: Web framework migration (Laravel/Rails); shared Composer/Bundler patterns
- lua-to-rust: Embedded scripting and DSL migration; dynamic typing to struct/enum
- nodejs-to-rust: Async runtime migration; shared EventMachine/tokio patterns
- java-to-rust: Enterprise patterns; Spring Boot/Rails service architecture
- go-to-rust: Goroutine/fiber concurrency mapping; shared M:N scheduling concepts
- kotlin-to-rust: Shared null-safety and expression-oriented programming patterns