DEV Community

Cover image for Trait-Driven Rust Architecture
Ramin Farajpour Cami
Ramin Farajpour Cami

Posted on

Trait-Driven Rust Architecture

Trait-Driven Rust Architecture

This project demonstrates clean architecture principles in Rust using traits, structured error handling, and domain modeling. It's designed to showcase professional Rust development patterns and best practices.

Key Features

  • Trait-first Architecture: Contracts separated from implementation details
    • Feature-gated Serialization: Choose your serialization format (JSON or Bincode)
    • Robust Error Handling: Clear error management using thiserror
    • Domain Modeling: Business models with state enums
    • Clean Layering: Well-separated application layers

Building and Running

# Run tests with bincode serialization
cargo test --features bincode

# Run tests with JSON serialization  
cargo test --features json

# Run CLI application with bincode
cargo run --bin cli --features "cli bincode"

# Run CLI application with JSON
cargo run --bin cli --features "cli json"

# Check syntax and compilation
cargo check --features bincode
Enter fullscreen mode Exit fullscreen mode

Project Structure

traity-app/
├── src/
│   ├── lib.rs              # Main library entry point
│   ├── prelude.rs          # Common imports and re-exports
│   ├── error.rs            # ErrorCode definitions and Result types
│   ├── traits/             # Trait definitions
│   │   ├── serialize.rs    # MySerialize/MyDeserialize traits
│   │   └── owner.rs        # Owner trait for access control
│   ├── domain/             # Domain models and business logic
│   │   ├── user.rs         # User struct and methods
│   │   ├── vault.rs        # Vault struct with business rules
│   │   └── state.rs        # VaultState enum for state management
│   └── infra/              # Infrastructure layer
│       └── storage.rs      # Storage trait and implementations
├── bin/
│   └── cli.rs              # Command-line interface example
└── tests/
    └── integration.rs      # Integration tests
Enter fullscreen mode Exit fullscreen mode

Learning Concepts

1. Trait-based Design Pattern

Traits in Rust define shared behavior without specifying implementation details:

pub trait MySerialize {
    fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<()>;
}

pub trait Owner {
    type OwnerId: Copy + Eq + Debug;
    fn owner_id(&self) -> Self::OwnerId;
}
Enter fullscreen mode Exit fullscreen mode

Why use traits?

  • Flexibility: Different types can implement the same behavior differently
    • Testability: Easy to mock dependencies in tests
    • Extensibility: Add new implementations without changing existing code

2. Feature-gated Implementations

Rust's feature flags allow conditional compilation:

#[cfg(feature = "bincode")]
mod bincode_impls {
    // Implementation using bincode for binary serialization
}

#[cfg(feature = "json")]  
mod json_impls {
    // Implementation using JSON for text serialization
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Reduced dependencies: Only compile what you need
    • Flexibility: Choose serialization format at compile time
    • Performance: Binary vs. human-readable trade-offs

3. State Management with Enums

Rust enums are powerful for modeling business states:

pub enum VaultState {
    Uninitialized,
    Active { frozen: bool },
    Closed,
}

// Usage with pattern matching
impl Vault {
    pub fn deposit(&mut self, amount: u64) -> Result<()> {
        match self.state {
            VaultState::Active { frozen: false } => {
                self.balance += amount;
                Ok(())
            }
            VaultState::Active { frozen: true } => {
                Err(ErrorCode::VaultFrozen)
            }
            _ => Err(ErrorCode::InvalidState),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Type safety: Compiler ensures all states are handled
    • Clarity: Business rules are explicit in code
    • Maintainability: Easy to add new states and transitions

4. Structured Error Handling

Using thiserror for clean error definitions:

#[derive(Debug, Error)]
pub enum ErrorCode {
    #[error("Invalid data format")]
    InvalidData,

    #[error("Missing required field: {0}")]
    MissingField(&'static str),

    #[error("Not owner of this resource")]
    NotOwner,

    #[error("Vault is currently frozen")]
    VaultFrozen,

    #[error("Invalid state for this operation")]
    InvalidState,
}
Enter fullscreen mode Exit fullscreen mode

Best practices:

  • Descriptive messages: Help users understand what went wrong
    • Categorization: Group related errors for better handling
    • Context: Include relevant information in error variants

Getting Started

Step 1: Clone and Build

git clone <your-repo>
cd traity-app
cargo build --features bincode
Enter fullscreen mode Exit fullscreen mode

Step 2: Run Tests

# Test core functionality
cargo test --features bincode -- --nocapture

# Test with different serialization
cargo test --features json -- --nocapture
Enter fullscreen mode Exit fullscreen mode

Step 3: Try the CLI

# Create a new vault and perform operations
cargo run --bin cli --features "cli bincode"
Enter fullscreen mode Exit fullscreen mode

Github : https://github.com/raminfp/trait-driven-rust-architecture

Learning Resources

Essential Reading

Advanced Topics

Top comments (0)