Clean Architecture
Implementing Clean Architecture principles in Rust for maintainable applications.
Clean Architecture
Clean Architecture organizes code into concentric layers with dependencies pointing inward toward the domain.
Architecture Layers
1. Domain Layer (Core)
Innermost layer - Pure business logic, no dependencies
RUST// src/domain/mod.rs pub mod entities; pub mod use_cases; pub mod traits; // src/domain/entities/user.rs pub struct User { pub id: UserId, pub email: Email, pub balance: Balance, } impl User { pub fn transfer_to(&mut self, recipient: &mut User, amount: u64) -> Result<(), Error> { // Pure business logic if self.balance < amount { return Err(Error::InsufficientFunds); } self.balance -= amount; recipient.balance += amount; Ok(()) } } // src/domain/use_cases/transfer_funds.rs pub struct TransferFundsUseCase; impl TransferFundsUseCase { pub fn execute( &self, user_repo: &dyn UserRepository, from: UserId, to: UserId, amount: u64 ) -> Result<(), Error> { // Use case orchestration let mut from_user = user_repo.find_by_id(from)?; let mut to_user = user_repo.find_by_id(to)?; from_user.transfer_to(&mut to_user, amount)?; user_repo.save(&from_user)?; user_repo.save(&to_user)?; Ok(()) } } // src/domain/traits/user_repository.rs pub trait UserRepository: Send + Sync { fn find_by_id(&self, id: UserId) -> Result<User, Error>; fn save(&self, user: &User) -> Result<(), Error>; }
2. Application Layer
Use cases and application services - Depends on domain
RUST// src/application/mod.rs pub mod use_cases; pub mod dto; // src/application/use_cases/transfer_funds.rs use crate::domain::{UserRepository, TransferFundsUseCase}; pub struct TransferFundsService { user_repo: Box<dyn UserRepository>, use_case: TransferFundsUseCase, } impl TransferFundsService { pub fn execute(&self, from: UserId, to: UserId, amount: u64) -> Result<(), Error> { self.use_case.execute(&*self.user_repo, from, to, amount) } }
3. Infrastructure Layer
External concerns - Database, HTTP, file system - Depends on domain traits
RUST// src/infrastructure/mod.rs pub mod database; pub mod http; pub mod repositories; // src/infrastructure/repositories/user_repository_impl.rs use crate::domain::{User, UserRepository, UserId, Error}; use sqlx::PgPool; pub struct PostgresUserRepository { pool: PgPool, } impl UserRepository for PostgresUserRepository { fn find_by_id(&self, id: UserId) -> Result<User, Error> { // Database implementation // Convert DB row to domain entity } fn save(&self, user: &User) -> Result<(), Error> { // Save to database } }
4. Presentation Layer
API, CLI, Web - Depends on application layer
RUST// src/presentation/mod.rs pub mod api; pub mod cli; // src/presentation/api/routes.rs use crate::application::TransferFundsService; use axum::{Router, extract::State}; pub fn create_router(service: TransferFundsService) -> Router { Router::new() .route("/transfer", post(transfer_funds)) .with_state(service) } async fn transfer_funds( State(service): State<TransferFundsService>, Json(payload): Json<TransferRequest> ) -> Result<Json<()>, Error> { service.execute(payload.from, payload.to, payload.amount)?; Ok(Json(())) }
Project Structure
src/
āāā main.rs
āāā lib.rs
āāā domain/ # Core business logic
ā āāā mod.rs
ā āāā entities/ # Domain entities
ā ā āāā mod.rs
ā ā āāā user.rs
ā ā āāā transaction.rs
ā āāā use_cases/ # Business use cases
ā ā āāā mod.rs
ā ā āāā transfer_funds.rs
ā āāā traits/ # Repository traits
ā āāā mod.rs
ā āāā user_repository.rs
āāā application/ # Application services
ā āāā mod.rs
ā āāā use_cases/
ā āāā dto/
āāā infrastructure/ # External implementations
ā āāā mod.rs
ā āāā database/
ā āāā repositories/ # Repository implementations
ā āāā http/
āāā presentation/ # API/CLI/Web
āāā mod.rs
āāā api/
āāā cli/
Dependency Rule
Dependencies point inward:
Presentation ā Application ā Domain ā Infrastructure
- Domain: No dependencies (pure Rust)
- Application: Depends on Domain
- Infrastructure: Depends on Domain (implements traits)
- Presentation: Depends on Application
Benefits
- Independence: Domain logic independent of frameworks
- Testability: Easy to test domain without infrastructure
- Flexibility: Swap implementations (DB, HTTP framework)
- Maintainability: Clear boundaries and responsibilities
- Longevity: Business logic survives technology changes
Code Examples
Clean Architecture Structure
Clean architecture with trait-based dependencies
pub trait UserRepository {
fn find(&self, id: u64) -> Result<User, Error>;
}
pub struct User {
pub id: u64,
pub name: String,
}
pub struct UserService {
repo: Box<dyn UserRepository>,
}
impl UserService {
pub fn get_user(&self, id: u64) -> Result<User, Error> {
self.repo.find(id)
}
}
pub struct PostgresUserRepository;
impl UserRepository for PostgresUserRepository {
fn find(&self, id: u64) -> Result<User, Error> {
Ok(User { id, name: String::from("User") })
}
}
fn main() {
println!("Clean Architecture:");
println!("1. Domain: Pure business logic");
println!("2. Application: Use cases");
println!("3. Infrastructure: External implementations");
println!("4. Presentation: API/CLI");
}Explanation:
Clean Architecture uses traits to invert dependencies. Infrastructure implements domain traits, keeping domain independent of external concerns.
Trait-Based Design
Using traits for dependency inversion
pub trait PaymentProcessor {
fn process_payment(&self, amount: u64) -> Result<(), Error>;
}
pub struct Order {
processor: Box<dyn PaymentProcessor>,
}
impl Order {
pub fn pay(&self, amount: u64) -> Result<(), Error> {
self.processor.process_payment(amount)
}
}
pub struct StripeProcessor;
impl PaymentProcessor for StripeProcessor {
fn process_payment(&self, amount: u64) -> Result<(), Error> {
Ok(())
}
}
pub struct PayPalProcessor;
impl PaymentProcessor for PayPalProcessor {
fn process_payment(&self, amount: u64) -> Result<(), Error> {
Ok(())
}
}
fn main() {
println!("Trait-based design:");
println!("- Domain defines interface");
println!("- Infrastructure implements it");
println!("- Easy to swap implementations");
}Explanation:
Traits enable dependency inversion. The domain defines what it needs, and infrastructure provides implementations. This keeps domain independent.
Exercises
Create Repository Trait
Define a repository trait in domain and implement it in infrastructure!
Starter Code:
pub trait UserRepository {
fn find(&self, id: u64) -> Option<String>;
}
pub struct InMemoryUserRepository;
fn main() {
let repo = InMemoryUserRepository;
let user = repo.find(1);
println!("{:?}", user);
}