DEV Community

Cover image for How Rust's Type System Enforces Business Rules at Compile Time
Aarav Joshi
Aarav Joshi

Posted on

How Rust's Type System Enforces Business Rules at Compile Time

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Rust's approach to type safety transforms how we model business rules. By leveraging its type system, we express domain constraints directly in our code. This technique catches inconsistencies before execution, leading to more robust applications. I've seen projects reduce defect rates significantly using these methods.

Consider payment processing systems. They require strict amount validation. Traditional approaches might check values at runtime, but Rust allows compile-time guarantees. Here's a practical implementation:

#[derive(Debug)]
struct PaymentAmount(f64);

impl PaymentAmount {
    fn new(value: f64) -> Result<Self, &'static str> {
        if !value.is_finite() {
            return Err("Amount must be finite");
        }
        if value <= 0.0 {
            return Err("Amount must be positive");
        }
        if value > 1_000_000.0 {
            return Err("Amount exceeds maximum");
        }
        Ok(Self(value))
    }

    fn value(&self) -> f64 {
        self.0
    }
}

fn process_transaction(amt: PaymentAmount, recipient: &str) {
    // Safe to use amt.value() here
    println!("Sending {} to {}", amt.value(), recipient);
}

// Usage
match PaymentAmount::new(250.0) {
    Ok(valid_amt) => process_transaction(valid_amt, "supplier"),
    Err(e) => eprintln!("Invalid amount: {}", e),
}
Enter fullscreen mode Exit fullscreen mode

This pattern prevents invalid amounts from entering our system. The compiler ensures only properly validated PaymentAmount instances exist. In my financial projects, this eliminated entire categories of rounding and overflow errors.

Unit confusion causes serious bugs in physical systems. Rust's type system prevents these through distinct measurement types:

#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
struct Fahrenheit(f64);

impl Celsius {
    fn to_fahrenheit(&self) -> Fahrenheit {
        Fahrenheit((self.0 * 9.0 / 5.0) + 32.0)
    }
}

fn monitor_temperature(temp: Celsius) {
    if temp.0 > 100.0 {
        println!("Critical temperature reached");
    }
}

// Compiler prevents mistakes
// monitor_temperature(Fahrenheit(212.0)); // Type mismatch error
Enter fullscreen mode Exit fullscreen mode

State transitions become explicit in Rust. We can model workflow stages as separate types:

struct MedicalPrescription {
    patient_id: u32,
    medication: String,
    dosage_mg: u32,
}

struct VerifiedPrescription {
    prescription: MedicalPrescription,
    physician_id: u32,
    verification_date: chrono::NaiveDate,
}

struct DispensedPrescription {
    verified: VerifiedPrescription,
    pharmacist_id: u32,
    dispense_date: chrono::NaiveDate,
}

impl MedicalPrescription {
    fn verify(self, physician_id: u32) -> VerifiedPrescription {
        VerifiedPrescription {
            prescription: self,
            physician_id,
            verification_date: chrono::Local::now().date_naive(),
        }
    }
}

impl VerifiedPrescription {
    fn dispense(self, pharmacist_id: u32) -> DispensedPrescription {
        DispensedPrescription {
            verified: self,
            pharmacist_id,
            dispense_date: chrono::Local::now().date_naive(),
        }
    }
}

// Usage flow
let script = MedicalPrescription {
    patient_id: 4567,
    medication: "Insulin".to_string(),
    dosage_mg: 10,
};

let verified = script.verify(1234); // Physician ID
let dispensed = verified.dispense(5678); // Pharmacist ID

// Compiler prevents invalid sequences:
// script.dispense(5678); // Error: MedicalPrescription has no dispense method
// dispensed.verify(1234); // Error: Wrong state
Enter fullscreen mode Exit fullscreen mode

This pattern enforces correct workflow sequences. I've applied similar techniques in logistics software, where package states must transition sequentially from "warehouse" to "in-transit" to "delivered".

Access control benefits from Rust's type safety. Consider permission hierarchies:

struct Guest;
struct User;
struct Admin;

impl Guest {
    fn view_content(&self) {
        println!("Displaying public content");
    }

    fn login(self, credentials: &str) -> Option<User> {
        if authenticate(credentials) {
            Some(User)
        } else {
            None
        }
    }
}

impl User {
    fn comment(&self, text: &str) {
        println!("Posting comment: {}", text);
    }

    fn logout(self) -> Guest {
        Guest
    }
}

impl Admin {
    fn delete_content(&self, content_id: u64) {
        println!("Deleting content {}", content_id);
    }
}

// Usage
let mut session = Guest;
session.view_content();

let user_session = session.login("user:pass").unwrap();
user_session.comment("Great article!");

// Compiler prevents privilege escalations:
// session.delete_content(123); // Error: Guest lacks method
// user_session.delete_content(123); // Error: User lacks method
Enter fullscreen mode Exit fullscreen mode

Phantom types provide powerful identity guarantees. The phantom_newtype crate helps distinguish identical underlying types:

use phantom_newtype::Id;

struct PatientTag;
struct DoctorTag;

type PatientId = Id<PatientTag, u32>;
type DoctorId = Id<DoctorTag, u32>;

struct Appointment {
    patient: PatientId,
    doctor: DoctorId,
    time: chrono::DateTime<Utc>,
}

fn schedule(patient: PatientId, doctor: DoctorId) -> Appointment {
    Appointment {
        patient,
        doctor,
        time: chrono::Utc::now() + chrono::Duration::days(7),
    }
}

// Prevents accidental swaps
// let appointment = schedule(doctor_id, patient_id); // Compile-time type error
Enter fullscreen mode Exit fullscreen mode

These techniques yield concrete benefits. Financial systems prevent invalid transactions through atomic state types. Medical applications encode dosage constraints directly in prescription types. Inventory systems ensure stock counts never go negative through unsigned integer wrappers.

The shift from runtime to compile-time validation reduces defensive coding. Business logic becomes simpler when invalid states are unrepresentable. Performance improves by moving checks upstream. Most importantly, we gain confidence that our core domain rules are enforced before code runs.

Rust's type system doesn't just prevent crashes - it helps model complex business realities accurately. By making constraints explicit in our types, we create systems where the compiler becomes an active partner in maintaining domain integrity. This approach has fundamentally changed how I design robust systems.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)