🎯

rust-hexagonal-architecture

🎯Skill

from oryjk/rust-hexagonal-architecture

VibeIndex|
What it does

Provides comprehensive Rust backend architecture guidelines using Hexagonal Architecture with vertical slicing for building maintainable, domain-driven applications.

rust-hexagonal-architecture

Installation

Quick InstallInstall with npx
npx skills add your-username/rust-hexagonal-architecture
Quick InstallInstall with npx
npx skills add https://raw.githubusercontent.com/your-username/rust-hexagonal-architecture/main/SKILL.md
Quick InstallInstall with npx
npx skills add ./rust-hexagonal-architecture.skill
πŸ“– Extracted from docs: oryjk/rust-hexagonal-architecture
2Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

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:

  1. Analyze the domain - identify business entities and their relationships
  2. Define the structure - create domain folders with vertical slicing
  3. Implement from core to edges: Domain β†’ Ports β†’ Application β†’ Adapters
  4. Verify with cargo check after every file edit
  5. 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 thiserror for 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 + Sync bounds
  • ❌ 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, u64)>;

}

```

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 CreateAssetUseCase {

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, DTOs
  • persistence/ - Database repositories, SQL queries
  • api/ - 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, // New optional field

}

// 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., asset not assets)

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.rs or verb_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::Error to 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 (use chrono::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

  1. βœ… Analyze domain β†’ define ports β†’ implement logic β†’ create adapters
  2. βœ… Run cargo check after every file edit
  3. βœ… Update docs/api.md if API changes
  4. βœ… 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 infrastructure folder name (use adapters)
  • Generic parameter explosion (>2-3 type params)

βœ… Always do these:

  • Handler β†’ UseCase β†’ Repository flow
  • Domain = pure Rust (no sqlx, axum, IO)
  • Use Arc for 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 rules
  • CLAUDE.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