Jump to content

Why Rewriting in Rust Won’t Fix Your Business Logic

From JOHNWICK

Last week, someone in our engineering Slack channel suggested rewriting our order processing service in Rust. “It’ll be faster,” they said. “Memory safe,” they added. “Zero-cost abstractions.”

I’ve seen this movie before. Different language, same plot. Five years ago, it was Go that would save us. Three years ago, it was microservices. Now it’s Rust.

Here’s what I’ve learned from watching rewrites: the language is rarely the problem. The Rust Pitch (And Why It’s Tempting)

I get the appeal. Rust 1.82 is genuinely impressive. Memory safety without garbage collection. Fearless concurrency. Performance comparable to C++. The borrow checker forces you to think about ownership.

A simple Rust HTTP handler looks clean:

// Rust 1.82 - Actix Web 4.9
use actix_web::{web, App, HttpResponse, HttpServer};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct OrderRequest {
    customer_id: i64,
    items: Vec<OrderItem>,
}

#[derive(Serialize)]
struct OrderResponse {
    order_id: String,
    total: f64,
}

async fn create_order(
    order: web::Json<OrderRequest>
) -> HttpResponse {
    // Business logic here
    let total = calculate_total(&order.items);
    
    HttpResponse::Ok().json(OrderResponse {
        order_id: generate_id(),
        total,
    })
}

Fast compilation feedback. Strong types. Async/await that actually works well. I understand why developers want this.

But here’s the problem: this code has the same bugs as your Python version.

The Real Problem Hiding in Your Codebase

Our order processing service had a critical bug. Orders occasionally got double-charged. Not often. Maybe 0.1% of transactions. Rare enough to slip through testing, common enough to generate support tickets.

The team blamed Python’s performance. “If we rewrite in Rust, we can add better locking,” they said.

I asked them to show me the business logic. Here’s what I found:

# Python 3.12 - The "slow" version
def process_order(customer_id: int, items: list) -> dict:
    # Check inventory
    for item in items:
        if not inventory.has_stock(item.id, item.quantity):
            raise OutOfStockError(f"Item {item.id} out of stock")
    
    # Calculate total
    total = sum(item.price * item.quantity for item in items)
    
    # Create order record
    order = Order.create(customer_id=customer_id, total=total)
    
    # Charge payment
    payment.charge(customer_id, total)
    
    # Deduct inventory
    for item in items:
        inventory.deduct(item.id, item.quantity)
    
    return {"order_id": order.id, "total": total}

See the bug? Between checking inventory and deducting it, another request can sneak in. Race condition. Classic. The proposed Rust rewrite looked like this:

// Rust 1.82 - The "fast" version (with the same bug)
async fn process_order(
    customer_id: i64,
    items: Vec<OrderItem>,
) -> Result<OrderResponse, OrderError> {
    // Check inventory
    for item in &items {
        if !inventory::has_stock(item.id, item.quantity).await? {
            return Err(OrderError::OutOfStock(item.id));
        }
    }
    
    // Calculate total
    let total: f64 = items.iter()
        .map(|item| item.price * item.quantity as f64)
        .sum();
    
    // Create order
    let order = Order::create(customer_id, total).await?;
    
    // Charge payment
    payment::charge(customer_id, total).await?;
    
    // Deduct inventory
    for item in &items {
        inventory::deduct(item.id, item.quantity).await?;
    }
    
    Ok(OrderResponse {
        order_id: order.id.to_string(),
        total,
    })
}

Same logic. Same bug. Different syntax. The borrow checker caught zero business logic errors because the business logic is wrong, not unsafe. The Architecture That Actually Matters The problem wasn’t the language. It was the transaction boundary.

Wrong Approach (Any Language)
══════════════════════════════

┌─────────────────────────────────────┐
│  Order Service                      │
│  ┌──────────────────────────────┐   │
│  │ 1. Check inventory           │   │
│  └──────────────────────────────┘   │
│           │                         │
│           ▼                         │
│  ┌──────────────────────────────┐   │
│  │ 2. Create order              │   │  ← Race condition
│  └──────────────────────────────┘   │    window here
│           │                         │
│           ▼                         │
│  ┌──────────────────────────────┐   │
│  │ 3. Charge payment            │   │
│  └──────────────────────────────┘   │
│           │                         │
│           ▼                         │
│  ┌──────────────────────────────┐   │
│  │ 4. Deduct inventory          │   │
│  └──────────────────────────────┘   │
└─────────────────────────────────────┘

The fix required rethinking the flow, not the language:

Correct Approach (Transactional)
════════════════════════════════

┌──────────────────────────────────────────────┐
│  Order Service                               │
│  ┌────────────────────────────────────────┐  │
│  │   BEGIN TRANSACTION                    │  │
│  │                                        │  │
│  │   1. Lock inventory rows               │  │
│  │   2. Verify stock                      │  │
│  │   3. Create order                      │  │
│  │   4. Deduct inventory                  │  │
│  │                                        │  │
│  │   COMMIT                               │  │
│  └────────────────────────────────────────┘  │
│           │                                  │
│           ▼                                  │
│  ┌────────────────────────────────────────┐  │
│  │   Async: Charge payment                │  │
│  │   Async: Send confirmation             │  │
│  └────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘

We used PostgreSQL’s SELECT FOR UPDATE with proper transaction isolation. Works in Python, Go, Rust, or any language with a decent database driver.

# Python 3.12 - Fixed version
def process_order(customer_id: int, items: list) -> dict:
    with db.transaction() as tx:
        # Lock and verify inventory atomically
        for item in items:
            stock = tx.execute(
                "SELECT quantity FROM inventory "
                "WHERE product_id = %s FOR UPDATE",
                (item.id,)
            ).fetchone()
            
            if stock['quantity'] < item.quantity:
                raise OutOfStockError(f"Item {item.id} out of stock")
        
        # Create order
        order = tx.create_order(customer_id, items)
        
        # Deduct inventory (still in transaction)
        for item in items:
            tx.execute(
                "UPDATE inventory SET quantity = quantity - %s "
                "WHERE product_id = %s",
                (item.quantity, item.id)
            )
        
        tx.commit()
    
    # Async operations outside transaction
    payment.charge_async(customer_id, order.total)
    notifications.send_async(customer_id, order.id)
    
    return {"order_id": order.id, "total": order.total}

This fixed the bug. In Python. No rewrite needed. The Performance Reality Check

“But Rust is faster!” they said. So I benchmarked both implementations handling 1000 concurrent orders:

Metric                 Python 3.12 (Fixed)   Rust 1.82 (Buggy)   Rust 1.82 (Fixed)
---------------------------------------------------------------------------------
Throughput             850 req/s             2,400 req/s          870 req/s
P99 Latency            145 ms                52 ms                142 ms
Memory                 320 MB                85 MB                95 MB
Double Charges         0                     ~2 per 10,000        0

The buggy Rust version was indeed faster. It was also wrong. When we fixed the business logic in Rust using proper transactions, performance matched Python almost exactly.

Why? Because the bottleneck was database I/O, not CPU. The transaction isolation and locking consumed the same time regardless of language.

Request Lifecycle Breakdown
═══════════════════════════

Total Time: 142ms

Database Transaction: 118ms (83%)
├─ Acquire locks: 45ms
├─ Verify inventory: 28ms
├─ Create order: 22ms
└─ Update inventory: 23ms

Application Logic: 18ms (13%)
├─ Parse request: 3ms
├─ Validate data: 2ms
└─ Business rules: 13ms

Network I/O: 6ms (4%)

Application logic consumed 13% of request time. That’s the part Rust would optimize. The 83% spent in database operations? Identical across languages.

What Actually Needs Fixing After years in backend development, I’ve learned that most performance problems come from:

  • Missing indexes: A single index can turn a 2000ms query into 8ms
  • N+1 queries: Loading relationships in loops instead of joins
  • Lack of caching: Hitting the database for static data
  • Poor database design: Normalization issues or schema mismatches
  • Wrong transaction boundaries: Like our inventory race condition

None of these are solved by switching languages. Before considering a rewrite, I ask:

  • Have we profiled the actual bottlenecks?
  • Are we using database features correctly?
  • Do we have proper monitoring?
  • Is the business logic actually correct?

Usually, the answer reveals that the rewrite is premature optimization disguised as technical improvement. When Rust Actually Makes Sense Don’t get me wrong. Rust has legitimate use cases:

  • Systems programming where memory safety is critical
  • High-throughput data processing (parsers, serializers)
  • WebAssembly for browser performance
  • Replacing C/C++ in existing codebases
  • CPU-bound workloads with complex concurrency

But CRUD APIs? Microservices making database calls? That’s not where Rust shines. The database is your bottleneck, not the application layer. The Hard Truth Rewrites fail because they focus on technology instead of problems. The real issues are:

  • Unclear requirements
  • Poor architecture
  • Missing tests
  • Lack of monitoring
  • Incorrect business logic

A rewrite in Rust, Go, or any language will inherit these problems while adding:

  • Months of development time
  • Learning curve for the team
  • New classes of bugs
  • Opportunity cost of features not built

Fix your business logic first. Profile your actual bottlenecks. Add proper transaction handling. Then, if CPU is genuinely your constraint, consider Rust. But in my experience, it almost never is.

Read the full article here: https://medium.com/@harishsingh8529/why-rewriting-in-rust-wont-fix-your-business-logic-5dd5389bafd9