Clean Architecture

Implementing Clean Architecture principles in Rust for maintainable applications.

Advancedā±ļø 50 minšŸ“š Prerequisites: 1

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

RUST
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

RUST
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!

Medium

Starter Code:

RUST
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);
}