DEV Community

Mrakdon
Mrakdon

Posted on

Clean Architecture in Rust: Structure and Implementation

Rustic Clean Architecture: Building Maintainable Systems the Safe Way

"Architecture is the art of making systems that last. In Rust, we make them last forever." – Your friendly neighborhood systems programmer

Why Rust Loves Clean Architecture

Rust's ownership model and zero-cost abstractions make it a natural fit for Clean Architecture. When you combine Rust's compile-time safety with Uncle Bob's architectural principles, you get:

  • Unbreakable boundaries between layers
  • Testable business logic that doesn't touch databases
  • Fearless refactoring with compiler-enforced contracts

[!NOTE]
Rust's #[derive] and trait system make interface adapters feel like second nature

The Rustic Layer Cake

Let's look at how we structure a Rust project with Clean Architecture:

graph TD
    A[Entities] --> B[Use Cases]
    B --> C[Interface Adapters]
    C --> D[Frameworks & Drivers]

    style A fill:#239384,stroke:#333
    style B fill:#49A235,stroke:#333
    style C fill:#8E8830,stroke:#333
    style D fill:#E95420,stroke:#333

    classDef core fill:#239384,stroke:#333;
    classDef business fill:#49A235,stroke:#333;
    classDef interface fill:#8E8830,stroke:#333;
    classDef infrastructure fill:#E95420,stroke:#333;
Enter fullscreen mode Exit fullscreen mode
Layer Rust Superpower Example Pattern
Entities #[derive(Clone, Debug)] Value objects
Use Cases Trait objects + Arc Business logic orchestrators
Interface Adapters axum/warp HTTP translation layer
Frameworks sqlx/tokio Database/async glue

Project Structure That Won't Make You Cry

Here's how I organize my Rust projects:

project-root/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── domain/          # Entities + Use Cases
│   │   └── user.rs
│   ├── application/     # Interface Adapters
│   │   ├── web/
│   │   │   └── handlers.rs
│   │   └── dto/
│   │       └── user_dto.rs
│   └── infrastructure/  # Frameworks & Drivers
│       ├── database/
│       │   └── user_repository.rs
│       └── config/
│           └── settings.rs
└── tests/
    ├── unit/
    └── integration/
Enter fullscreen mode Exit fullscreen mode

[!WARNING]
Never let your database code touch your business logic. That's like letting your dog drive the car - it might work, but you'll end up lost.

Real-World Example: User Registration

Let's build a user registration system that could survive a nuclear winter:

1. Domain Layer (Business Rules)

// src/domain/user.rs
#[derive(Debug, Clone)]
pub struct User {
    pub id: uuid::Uuid,
    pub email: String,
    pub created_at: chrono::NaiveDateTime,
}

pub trait UserRepository: Send + Sync {
    fn create(&self, email: String) -> Result<User, anyhow::Error>;
}
Enter fullscreen mode Exit fullscreen mode

2. Application Layer (Use Case)

// src/application/use_cases/user.rs
use std::sync::Arc;

pub struct RegisterUserUseCase {
    user_repo: Arc<dyn UserRepository>,
}

impl RegisterUserUseCase {
    pub fn new(user_repo: Arc<dyn UserRepository>) -> Self {
        Self { user_repo }
    }

    pub fn execute(&self, email: String) -> Result<User, anyhow::Error> {
        self.user_repo.create(email)
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Interface Adapter (Web Layer)

// src/application/web/handlers.rs
use axum::{Json, extract::State};
use std::sync::Arc;

#[derive(serde::Deserialize)]
pub struct RegisterUserRequest {
    pub email: String,
}

pub async fn register_user_handler(
    State(use_case): State<Arc<RegisterUserUseCase>>,
    Json(payload): Json<RegisterUserRequest>,
) -> Result<Json<User>, (axum::http::StatusCode, String)> {
    use_case.execute(payload.email).map(Json)
}
Enter fullscreen mode Exit fullscreen mode

Pro Tips for Rustaceans

  1. Trait objects for loose coupling
   pub trait ExternalService: Send + Sync {
       fn fetch_data(&self) -> Result<String, anyhow::Error>;
   }
Enter fullscreen mode Exit fullscreen mode
  1. Mocking made easy
   struct MockUserRepository {
       users: Arc<Mutex<Vec<User>>>,
   }

   impl UserRepository for MockUserRepository {
       fn create(&self, email: String) -> Result<User, anyhow::Error> {
           let user = User {
               id: Uuid::new_v4(),
               email,
               created_at: chrono::Utc::now().naive_utc(),
           };
           self.users.lock().unwrap().push(user.clone());
           Ok(user)
       }
   }
Enter fullscreen mode Exit fullscreen mode
  1. Error handling with style
   [dependencies]
   thiserror = "1.0.33"
   anyhow = "1.0.70"
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls to Avoid

[!WARNING]
These are the architectural sins that will make your codebase a maintenance nightmare:

  1. Circular dependencies – Use dependency injection to break cycles
  2. Layer violations – Never import infrastructure into domain
  3. Premature optimization – Start with simple structs, add complexity only when needed
graph LR
    A[Clean Architecture] --> B[Modular Design]
    B --> C[Testable Code]
    C --> D[Scalable System]
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

When you combine Rust's safety guarantees with Clean Architecture principles, you get a system that:

  • Compiles with confidence
  • Tests with ease
  • Scales without breaking

[!NOTE]
Start small, keep your layers pure, and let the compiler be your best friend. Happy coding, Rustaceans!
Try Mrakdon free → Sign up at mrakdon.com and automate your technical writing workflow.

Top comments (0)