rust-hexagonal-architecture
π―Skillfrom oryjk/rust-hexagonal-architecture
Provides comprehensive Rust backend architecture guidelines using Hexagonal Architecture with vertical slicing for building maintainable, domain-driven applications.
Installation
npx skills add your-username/rust-hexagonal-architecturenpx skills add https://raw.githubusercontent.com/your-username/rust-hexagonal-architecture/main/SKILL.mdnpx skills add ./rust-hexagonal-architecture.skillSkill Details
Complete implementation guide for Rust projects using Hexagonal Architecture with vertical slicing. Use when working with Rust/Axum/PostgreSQL projects that require clean architecture principles, domain-driven design, or when implementing new features following strict architectural patterns. Essential for maintaining code quality and architectural consistency in backend projects.
Overview
# Rust Hexagonal Architecture
Complete architectural guidelines for building maintainable Rust backend applications using Hexagonal Architecture with vertical slicing.
Quick Start
When starting work on a Rust project using this architecture:
- Analyze the domain - identify business entities and their relationships
- Define the structure - create domain folders with vertical slicing
- Implement from core to edges: Domain β Ports β Application β Adapters
- Verify with
cargo checkafter every file edit - Document API changes in
docs/api.md
Architecture Overview
Core Principles
Hexagonal Architecture (Ports and Adapters) with Vertical Slicing:
- Business Logic isolated in the core domain layer
- External Dependencies handled by adapters
- Communication through port interfaces (traits)
- Domains organized vertically by business capability
Project Structure
```
src/
βββ admin/ # Business domain (e.g., admin management)
β βββ domain/ # Core business logic
β β βββ entities.rs
β β βββ enums.rs
β β βββ mod.rs
β βββ ports/ # Interface definitions
β β βββ repository.rs
β β βββ mod.rs
β βββ application/ # Use cases and orchestration
β β βββ actions.rs
β β βββ mod.rs
β βββ adapters/ # External implementations
β βββ web/ # HTTP handlers
β β βββ handlers.rs
β β βββ routes.rs
β β βββ dto.rs
β βββ persistence/ # Database access
β βββ postgres_repository.rs
βββ assets/
βββ sync/
βββ user/
βββ bin/ # Migration scripts
```
Layer Responsibilities
Domain Layer (`src/{domain}/domain/`)
What it does:
- Define business entities (structs)
- Implement business logic and validation
- Define domain-specific enums and value objects
Constraints:
- β Pure Rust - no external dependencies
- β
Can use
thiserrorfor error definitions - β NO
sqlx,axum, or IO operations - β NO database-specific code
Example:
```rust
// src/assets/domain/asset.rs
pub struct Asset {
pub code: String,
pub name: String,
pub asset_type: AssetType,
}
impl Asset {
pub fn new(code: String, name: String, asset_type: AssetType) -> Result
if code.is_empty() {
return Err(DomainError::InvalidCode);
}
Ok(Self { code, name, asset_type })
}
}
```
Ports Layer (`src/{domain}/ports/`)
What it does:
- Define repository interfaces (traits)
- Define service interfaces
- Establish contracts between layers
Constraints:
- β
Use
#[async_trait]for async methods - β
Require
Send + Syncbounds - β NO implementation code
Example:
```rust
// src/assets/ports/repository.rs
#[async_trait]
pub trait AssetRepository: Send + Sync {
async fn find_by_code(&self, code: &str) -> Result
async fn save(&self, asset: &Asset) -> Result
async fn list(&self, pagination: &Pagination) -> Result<(Vec
}
```
Application Layer (`src/{domain}/application/`)
What it does:
- Implement use cases (orchestration)
- Coordinate multiple repositories
- Execute business workflows
Constraints:
- β Call Port interfaces (not concrete implementations)
- β Orchestrate business logic
- β NO SQL queries
- β NO direct database access
Example:
```rust
// src/assets/application/actions.rs
pub struct CreateAssetUseCase
repository: R,
}
impl
pub async fn execute(&self, request: CreateAssetRequest) -> Result
// Business logic
let asset = Asset::new(request.code, request.name, request.asset_type)?;
// Persistence through port
self.repository.save(&asset).await
}
}
```
Adapters Layer (`src/{domain}/adapters/`)
What it does:
- Implement port interfaces
- Handle external concerns (HTTP, Database)
- Map between DTOs and domain entities
Subdirectories:
web/- HTTP handlers, routes, DTOspersistence/- Database repositories, SQL queriesapi/- External API clients
Example:
```rust
// src/assets/adapters/persistence/postgres_repository.rs
#[derive(Clone)]
pub struct PostgresAssetRepository {
pool: PgPool,
}
#[async_trait]
impl AssetRepository for PostgresAssetRepository {
async fn find_by_code(&self, code: &str) -> Result
let row = sqlx::query_as::<_, (String, String, AssetType)>(
"SELECT code, name, asset_type FROM b_assets WHERE code = $1"
)
.bind(code)
.fetch_optional(&*self.pool)
.await?;
Ok(row.map(|(code, name, asset_type)| Asset { code, name, asset_type }))
}
}
```
Critical Rules
1. Dependency Direction
Rule: Dependencies flow from outer to inner layers only.
```
Web Adapter β Application UseCase β Port Interface β Domain Entity
β β
βββββββββββββββ Persistence Adapter βββββββββ
```
Violations to avoid:
- β Domain layer depending on Adapter
- β Handler calling Repository directly (must use UseCase)
- β UseCase containing SQL queries
2. Handler β UseCase β Repository Flow
Required flow for all HTTP requests:
```
HTTP Request
β
Handler (parse request, validate HTTP)
β
UseCase (business logic, orchestration)
β
Repository (data access via Port interface)
β
Database
```
Handler responsibilities:
- β Parse HTTP request (Path, Query, Body)
- β Validate HTTP-level constraints
- β Convert DTO to UseCase request
- β Call UseCase
- β Convert error to HTTP status
- β NO business logic
- β NO direct database access
- β NO repository calls
3. Generic vs Trait Object
Default preference: Use Arc over generics.
Use generics when:
- Performance-critical paths
- Need compile-time type safety
- β€2-3 type parameters
Use trait objects when:
- Need flexibility
- Avoiding generic explosion (>2-3 type params)
- Type determined at runtime
Example:
```rust
// β Preferred - trait object
pub struct AssetState {
create_asset_use_case: Arc
get_asset_use_case: Arc
}
// β Acceptable - generics (performance-critical)
pub struct GetAssetUseCase
repository: Arc
}
// β Avoid - generic explosion
pub struct ComplexUseCase
```
4. Schema Changes and UseCase Modifications
Rule: Distinguish between structural changes and business logic changes when modifying database schemas.
Scenario A: Pure Structural Changes
Adding optional fields or changing types without affecting business logic (e.g., adding Option for display data).
| Layer | Modify? | Why? |
|------------|---------|------------------------------------------|
| Domain | β Yes | Add/update field in entity |
| Ports | β No | Interface unchanged |
| Application| β No | Business logic unchanged (structural only) |
| Adapters | β Yes | Update SQL + field mapping |
Example: Adding optional dividend_yield to Asset
```rust
// Domain: Add field
pub struct Asset {
pub code: String,
pub name: String,
pub dividend_yield: Option
}
// UseCase: No changes needed
pub async fn execute(&self, code: &str) -> Result
self.repository.find_by_code(code).await? // Logic unchanged
.ok_or_else(|| anyhow!("Asset not found"))
}
```
Scenario B: Business Logic Changes
Adding fields that require new validation, workflows, or affect existing functionality.
| Layer | Modify? | Why? |
|------------|---------|------------------------------------------|
| Domain | β Yes | Add/update field in entity |
| Ports | β No | Interface unchanged |
| Application| β Yes | Business logic changed |
| Adapters | β Yes | Update SQL + field mapping |
Example: Adding dividend_yield with validation
```rust
// UseCase: Add validation logic
pub async fn execute(&self, code: &str) -> Result
let asset = self.repository.find_by_code(code).await?
.ok_or_else(|| anyhow!("Asset not found"))?;
// New business logic
if let Some(yield) = asset.dividend_yield {
if yield < 0.0 {
return Err(anyhow!("Dividend yield cannot be negative"));
}
}
Ok(asset)
}
```
Key Principle: Separate structural changes from functional changes.
Coding Standards
Naming Conventions
Directories:
- Domain names: lowercase (e.g.,
admin,assets,user) - Use singular form (e.g.,
assetnotassets)
Database tables:
- Prefix:
b_(e.g.,b_assets,b_users) - Format: lowercase with underscores
Files:
- Entities:
entity_name.rs(e.g.,asset.rs) - UseCases:
actions.rsorverb_entity.rs - Repositories:
{database}_entity_repository.rs
Code:
- Structs/Enums:
PascalCase(e.g.,Asset,AssetType) - Functions:
snake_case(e.g.,find_by_code) - Constants:
SCREAMING_SNAKE_CASE
Error Handling
Layer-specific error handling:
- Domain: Define business errors with
thiserror - Repository: Map
sqlx::Errorto domain errors (don't expose DB errors) - Application/Web: Use
anyhow, map to HTTP status
```rust
// Domain layer
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DomainError {
#[error("Invalid asset code")]
InvalidCode,
#[error("Asset not found: {0}")]
NotFound(String),
}
// Application layer
use anyhow::Result;
pub async fn execute(&self, code: &str) -> Result
self.repository.find_by_code(code).await?
.ok_or_else(|| anyhow::anyhow!("Asset not found: {}", code))
}
```
Database Standards
- All timestamp fields:
TIMESTAMPTZ(usechrono::DateTime) - Use parameterized queries:
$1,$2 - Transactions for multi-step operations
- All tables start with
b_prefix
API Documentation
Critical: When adding/modifying APIs, MUST update docs/api.md
Include:
- HTTP method and path
- Request parameters (path, query, body)
- Response format (success and error)
- Field descriptions and examples
Development Workflow
Mandatory Checklist
- β Analyze domain β define ports β implement logic β create adapters
- β
Run
cargo checkafter every file edit - β
Update
docs/api.mdif API changes - β Commit only after all checks pass
Validation Commands
```bash
# Required after every file edit
cargo check
# Before committing
cargo build
cargo clippy
cargo test
# Update documentation
# docs/api.md
```
Anti-Patterns to Avoid
β Never do these:
- Handler calling Repository directly
- Domain struct with
#[derive(sqlx::FromRow)] - Using
infrastructurefolder name (useadapters) - Generic parameter explosion (>2-3 type params)
β Always do these:
- Handler β UseCase β Repository flow
- Domain = pure Rust (no sqlx, axum, IO)
- Use
Arcfor Handler State - Distinguish between structural and business logic changes
Reference Documentation
For detailed architectural rules, naming conventions, and best practices, see:
- [PROJECT_STANDARDS.md](references/PROJECT_STANDARDS.md) - Complete project standards and guidelines
- [CLAUDE.md](references/CLAUDE.md) - Project identity and workflow guidelines
Resources
References/
PROJECT_STANDARDS.md- Detailed coding standards and architecture rulesCLAUDE.md- Project identity, workflow, and command reference
These references provide in-depth guidance for specific aspects of the architecture and should be consulted when implementing complex features or clarifying architectural decisions.
---
Usage Notes:
- This skill applies to any Rust backend project using Hexagonal Architecture
- When creating new domains, copy structure from existing domains (e.g.,
user/) - Always verify architecture compliance with
cargo check - API changes must be documented in
docs/api.md