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;
| 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/
[!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>;
}
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)
}
}
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)
}
Pro Tips for Rustaceans
- Trait objects for loose coupling
pub trait ExternalService: Send + Sync {
fn fetch_data(&self) -> Result<String, anyhow::Error>;
}
- 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)
}
}
- Error handling with style
[dependencies]
thiserror = "1.0.33"
anyhow = "1.0.70"
Common Pitfalls to Avoid
[!WARNING]
These are the architectural sins that will make your codebase a maintenance nightmare:
- Circular dependencies – Use dependency injection to break cycles
-
Layer violations – Never import
infrastructureintodomain - 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]
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)