Layered Architecture

Implementing layered architecture pattern in Rust applications.

Intermediateā±ļø 45 minšŸ“š Prerequisites: 1

Layered Architecture

Layered Architecture separates concerns into distinct layers, each with specific responsibilities.

Architecture Layers

1. Presentation Layer (API/Controllers)

Responsibility: Handle HTTP requests, validate input, format responses

RUST
// src/api/mod.rs
pub mod routes;
pub mod handlers;
pub mod middleware;

// src/api/handlers.rs
use crate::services::UserService;
use crate::models::User;

pub struct UserHandler {
    user_service: UserService,
}

impl UserHandler {
    pub async fn create_user(&self, data: CreateUserRequest) -> Result<User, Error> {
        // Validate input
        // Call service layer
        // Format response
        self.user_service.create_user(data).await
    }
}

2. Service Layer (Business Logic)

Responsibility: Implement business rules, orchestrate operations

RUST
// src/services/mod.rs
pub mod user_service;
pub mod transaction_service;

// src/services/user_service.rs
use crate::repositories::UserRepository;
use crate::models::User;

pub struct UserService {
    user_repo: UserRepository,
}

impl UserService {
    pub async fn create_user(&self, data: CreateUserRequest) -> Result<User, Error> {
        // Business logic
        // Validation rules
        // Call repository
        self.user_repo.create(data).await
    }
    
    pub async fn transfer_funds(
        &self,
        from: UserId,
        to: UserId,
        amount: u64
    ) -> Result<(), Error> {
        // Complex business logic
        // Multiple repository calls
        // Transaction management
    }
}

3. Repository Layer (Data Access)

Responsibility: Abstract database operations, data persistence

RUST
// src/repositories/mod.rs
pub mod user_repository;
pub mod transaction_repository;

// src/repositories/user_repository.rs
use crate::models::User;
use crate::database::Connection;

pub struct UserRepository {
    db: Connection,
}

impl UserRepository {
    pub async fn create(&self, data: CreateUserData) -> Result<User, Error> {
        // Database operations
        // SQL queries
        // Data mapping
    }
    
    pub async fn find_by_id(&self, id: UserId) -> Result<Option<User>, Error> {
        // Query database
    }
}

4. Domain/Model Layer

Responsibility: Define data structures, domain entities

RUST
// src/models/mod.rs
pub mod user;
pub mod transaction;

// src/models/user.rs
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: UserId,
    pub email: String,
    pub balance: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserRequest {
    pub email: String,
    pub password: String,
}

Complete Example Structure

src/
ā”œā”€ā”€ main.rs
ā”œā”€ā”€ lib.rs
ā”œā”€ā”€ api/                    # Presentation Layer
│   ā”œā”€ā”€ mod.rs
│   ā”œā”€ā”€ routes.rs           # Route definitions
│   ā”œā”€ā”€ handlers.rs         # Request handlers
│   └── middleware.rs       # Auth, logging, etc.
ā”œā”€ā”€ services/               # Service Layer
│   ā”œā”€ā”€ mod.rs
│   ā”œā”€ā”€ user_service.rs
│   └── transaction_service.rs
ā”œā”€ā”€ repositories/           # Repository Layer
│   ā”œā”€ā”€ mod.rs
│   ā”œā”€ā”€ user_repository.rs
│   └── transaction_repository.rs
ā”œā”€ā”€ models/                 # Domain Layer
│   ā”œā”€ā”€ mod.rs
│   ā”œā”€ā”€ user.rs
│   └── transaction.rs
ā”œā”€ā”€ database/               # Infrastructure
│   ā”œā”€ā”€ mod.rs
│   └── connection.rs
└── config/                 # Configuration
    └── mod.rs

Dependency Flow

API Layer
  ↓ (depends on)
Service Layer
  ↓ (depends on)
Repository Layer
  ↓ (depends on)
Database/Infrastructure

Key Principle: Dependencies flow downward. Upper layers depend on lower layers, but not vice versa.

Benefits

  • Separation of concerns: Each layer has clear responsibility
  • Testability: Easy to mock dependencies
  • Maintainability: Changes isolated to specific layers
  • Reusability: Services can be used by different APIs
  • Scalability: Layers can scale independently

Code Examples

Layered Architecture Example

Complete layered architecture structure

RUST
struct ApiHandler {
    service: UserService,
}
impl ApiHandler {
    fn handle_request(&self) {
        self.service.process();
    }
}
struct UserService {
    repo: UserRepository,
}
impl UserService {
    fn process(&self) {
        self.repo.save();
    }
}
struct UserRepository {
    db: Database,
}
impl UserRepository {
    fn save(&self) {
    }
}
struct User {
    id: u64,
    name: String,
}
fn main() {
    println!("Layered Architecture:");
    println!("1. API Layer: HTTP handling");
    println!("2. Service Layer: Business logic");
    println!("3. Repository Layer: Data access");
    println!("4. Domain Layer: Models");
}

Explanation:

Layered architecture separates concerns into distinct layers. Each layer has a specific responsibility and depends only on layers below it.

Service Layer Pattern

Service layer with business logic

RUST
struct TransferService {
    user_repo: UserRepository,
    tx_repo: TransactionRepository,
}
impl TransferService {
    fn transfer_funds(
        &self,
        from: u64,
        to: u64,
        amount: u64
    ) -> Result<(), String> {
        if amount == 0 {
            return Err(String::from("Amount must be positive"));
        }
        Ok(())
    }
}
fn main() {
    println!("Service layer contains:");
    println!("- Business rules");
    println!("- Validation logic");
    println!("- Orchestration");
}

Explanation:

The service layer contains business logic and orchestrates operations across multiple repositories. It's where complex business rules are implemented.

Exercises

Create Service Layer

Create a service struct with a method that uses a repository!

Medium

Starter Code:

RUST
struct UserService {
    repo: UserRepository,
}
struct UserRepository;
impl UserRepository {
    fn find_by_id(&self, id: u64) -> Option<String> {
        Some(format!("User {}", id))
    }
}
fn main() {
    let service = UserService::new(UserRepository);
    let user = service.get_user(1);
    println!("{:?}", user);
}