DEV Community

EDUARDO GINO FLORES NAVARRO
EDUARDO GINO FLORES NAVARRO

Posted on

Design First, Code Later: Mastering Spec-Driven Development in Rust

Have you ever started coding a feature, only to realize halfway through that your architecture is hopelessly tangled? We’ve all been there. You start writing a service, and before you know it, your business logic is heavily bleeding into your database queries and third-party APIs.

To prevent this architectural chaos, modern engineering teams are increasingly turning to Spec-Driven Development (SDD).

What is Spec-Driven Development?

Spec-Driven Development is a paradigm where you define the "What" (the specifications, contracts, and behaviors) long before you write the "How" (the concrete implementation).

Instead of diving straight into writing database queries or HTTP requests, you define clear interfaces. By doing this, you naturally enforce core Software Design Principles—most notably the Dependency Inversion Principle (DIP) and the Single Responsibility Principle (SRP). Your core application logic depends on abstractions (the Spec), not on concrete details.

How SDD Guides Correct Software Design

In languages with powerful type systems like Rust, SDD feels incredibly natural. Rust’s _trait _system is the perfect tool for defining specifications.

When you define a _trait _first, you are building a contract. Your business logic only knows about this contract. It doesn't care if the underlying data comes from a PostgreSQL database, an external REST API, or a simple text file. This separation of concerns allows different developers to work on the core logic and the infrastructure simultaneously, and it makes unit testing an absolute breeze.

The Example: Applying SDD to a Payment System

Let’s demonstrate how to apply Spec-Driven Development correctly in Rust. Imagine we are building an e-commerce checkout service.

Step 1: Define the Spec (The Contract)
Before writing any complex logic, we define what a payment gateway should do.

// 1. The Specification (Contract)
pub trait PaymentGateway {
    fn process_payment(&self, user_id: &str, amount: f64) -> Result<(), String>;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Write Logic Against the Spec
Now, we write our core business logic. Notice how _CheckoutService _doesn't know anything about Stripe, PayPal, or credit cards. It only knows about the _PaymentGateway _spec.

// 2. The Core Logic (Depends on the Spec, not the implementation)
pub struct CheckoutService<T: PaymentGateway> {
    gateway: T,
}

impl<T: PaymentGateway> CheckoutService<T> {
    pub fn new(gateway: T) -> Self {
        Self { gateway }
    }

    pub fn complete_checkout(&self, user_id: &str, amount: f64) {
        println!("🛒 Starting checkout process for user: {}", user_id);

        match self.gateway.process_payment(user_id, amount) {
            Ok(_) => println!("✅ Checkout successful! Items are being prepared."),
            Err(e) => eprintln!("❌ Checkout failed: {}", e),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Concrete Implementations
Finally, we implement the details. We can create a mock for local testing and a real one for production.

// 3. Concrete Implementations of the Spec

// A mock implementation for rapid testing and development
pub struct MockPaymentGateway;
impl PaymentGateway for MockPaymentGateway {
    fn process_payment(&self, _user_id: &str, amount: f64) -> Result<(), String> {
        println!("🛠️  [MOCK] Authorizing ${:.2} without hitting real APIs...", amount);
        Ok(())
    }
}

// A production implementation (e.g., Stripe)
pub struct StripeGateway;
impl PaymentGateway for StripeGateway {
    fn process_payment(&self, user_id: &str, amount: f64) -> Result<(), String> {
        println!("💳 [STRIPE] Connecting to production API for user {}...", user_id);
        println!("💳 [STRIPE] Successfully charged ${:.2}", amount);
        Ok(())
    }
}

fn main() {
    // Execution 1: Using the Mock (Development/Testing environment)
    println!("--- Running with Mock Gateway ---");
    let dev_service = CheckoutService::new(MockPaymentGateway);
    dev_service.complete_checkout("user_dev_01", 45.50);

    println!("\n--- Running with Production Gateway ---");
    // Execution 2: Using the Real Gateway (Production environment)
    let prod_service = CheckoutService::new(StripeGateway);
    prod_service.complete_checkout("user_prod_99", 120.00);
}
Enter fullscreen mode Exit fullscreen mode

Execution & Proof

Terminal Output:

Conclusion

  • Spec-Driven Development is more than just a coding technique; it is an architectural mindset. By defining your interfaces (specs) first, you are forced into building decoupled, highly cohesive systems.

  • In our Rust example, if we ever need to switch our payment provider from Stripe to another service, our CheckoutService _remains completely untouched. We simply write a new struct that implements the _PaymentGateway trait.

  • By marrying SDD with core software design principles, you stop fighting your codebase and start building modular, future-proof software. Define the contract, respect the boundaries, and let the architecture guide your implementation.

Top comments (0)