rust-architecture

star 2

Use when creating modules, organizing project structure, designing layered architecture (handler/service/domain/repo), dependency injection, or DDD patterns in Rust.

longzhi By longzhi schedule Updated 3/7/2026

name: rust-architecture description: > Use when creating modules, organizing project structure, designing layered architecture (handler/service/domain/repo), dependency injection, or DDD patterns in Rust.

Rust Architecture Standards

Code Organization

src/
├── core/                  # Business semantic kernel (core types, no external deps)
│   ├── mod.rs
│   └── error.rs

├── infra/                 # Shared infrastructure (technical, no business semantics)
│   ├── mod.rs
│   ├── db.rs              # DB pool / transaction
│   ├── auth.rs            # JWT / auth
│   └── config.rs

├── shared/                # Cross-module shared (non-infrastructure)
│   ├── mod.rs
│   ├── utils.rs           # Utility functions (date, string, etc.)
│   └── types.rs           # Public types, shared DTOs

├── modules/               # Business modules (Bounded Contexts, vertical slices)
│   ├── mod.rs
│   ├── order/
│   │   ├── mod.rs
│   │   ├── domain.rs      # Domain layer: domain models + domain services
│   │   ├── service.rs     # Application layer: coordinates domain + repo + infra
│   │   ├── repo.rs        # Infrastructure layer: data access
│   │   ├── handler.rs     # Presentation layer: HTTP handlers
│   │   └── infra/         # Infrastructure layer: module-specific (as needed)
│   │       ├── mod.rs
│   │       └── payment_client.rs
│   ├── product/
│   └── user/

├── server/                # Assembly / startup
│   ├── mod.rs
│   ├── router.rs
│   └── state.rs

├── main.rs                # Executable entry point
└── lib.rs                 # Library entry, declares module structure, re-exports

Module declaration style: Using mod.rs is recommended (avoids confusion from directory + same-name .rs file coexisting).

Module internal structure (each module follows this):

  • mod.rs — module declarations
  • domain.rs — Domain layer: domain models (aggregates, entities, value objects) + domain services (pure business logic across aggregates)
  • service.rs — Application layer: application services, coordinates domain, repo, and infra; handles transactions, authorization, auditing
  • repo.rs — Infrastructure layer: database access, SQL explicitly visible
  • handler.rs — Presentation layer: HTTP handlers, parameter parsing, response formatting
  • dto.rs — Presentation layer: Request/Response DTOs (optional; simple modules can inline in handler.rs)
  • infra/ — Infrastructure layer: module-specific infrastructure (as needed, not required)

DTO placement rules:

  • Module-internal DTOs go in dto.rs or inline in handler.rs
  • Cross-module shared DTOs go in shared/types.rs
  • DTOs are pure data structures for transport; they contain no business logic

DDD layer mapping:

handler.rs   → Presentation Layer
service.rs   → Application Layer
domain.rs    → Domain Layer
repo.rs      → Infrastructure Layer
infra/       → Infrastructure Layer

Key distinction:

  • domain.rs is pure Rust: no external dependencies, no database awareness
  • service.rs is the coordination layer: calls domain for business logic, calls repo for persistence

Relationship between module-level repo.rs and infra/:

service.rs
    ├── calls repo.rs      (database operations)
    └── calls infra/       (caches, external services, etc.)
  • repo.rs and infra/ are peers — both called by service, neither depends on the other
  • repo stays at module root because:
    • In DDD, the repository has special status as the persistence boundary of an aggregate
    • repo changes frequently with business evolution; other infra is relatively stable
    • Keeping repo at module root makes the data access layer immediately visible
  • infra/ is added as needed — not every module requires it

Top-level directory responsibilities:

Directory Responsibility Examples
core/ Business semantics, core types (minimal external deps) Core error types
shared/ Utility functions, public types, shared DTOs Date handling, validation
infra/ Shared infrastructure (module-specific infra goes in module infra/) DB pool, Redis, Auth

lib.rs purpose:

  • Declares module structure (pub mod core; pub mod infra; pub mod modules;)
  • Re-exports commonly used types
  • Keeps main.rs concise
  • Enables tests to reference the library directly

Architecture Patterns

Design Principle: Pragmatic DDD

The architecture follows DDD (Domain-Driven Design) style but avoids rigid adherence. Core principles:

Adopted DDD concepts:

  • Domain model first: Understand business concepts before designing code structure
  • Bounded Contexts: Divide modules by business boundary (modules/), not by technical layer
  • Aggregates and Aggregate Roots: Core business objects (Order, Product, User) have clear consistency boundaries
  • Repository pattern: Repo encapsulates data access; Domain has no knowledge of SQL
  • Domain vs Application services: Pure cross-aggregate business logic lives in domain.rs (domain service); coordination logic lives in service.rs (application service)

Pragmatic trade-offs:

  • No pursuit of pure Domain Models: If adding a database column saves complex domain logic, add the column
  • No forced event-driven architecture: Synchronous calls are fine for MVP; introduce domain events later as needed
  • Uniform structure, simple code: Even simple CRUD maintains the full handler -> service -> domain -> repo structure; simple cases just have less code per layer
  • Anemic models are acceptable: DTOs, simple config types can be plain data structures

Decision criterion:

Ask yourself: What benefit does this abstraction provide?

If the answer is "it's more DDD-compliant" → don't do it
If the answer is "easier to test / easier to change / easier to understand" → do it

Anti-patterns:

  • Do not introduce an interface/trait with only one implementation just for "decoupling"
  • Do not pre-design abstractions for "extensibility" you don't currently need
  • Do not implement features that aren't required yet for "completeness"
  • Do not over-complicate simple problems (e.g., turning a single field into a separate table + join query)

Summary: DDD is a tool, not a goal. Keep the system flexible and extensible enough, without over-engineering.

Dependency Injection and State Management

AppState organization: Single struct.

// src/server/state.rs
pub struct AppState {
    pub db: PgPool,
    pub redis: RedisPool,
    pub config: AppConfig,
    // Services
    pub order_service: Arc<OrderService>,
    pub user_service: Arc<UserService>,
    pub auth_service: Arc<AuthService>,
    // ...
}

Service dependency rules: Enforce the Service layer.

  • Handlers must NOT call Repo directly; always go through Service
  • Cross-cutting concerns (authorization, audit logging) are handled uniformly in Service
  • Services may call each other, but avoid circular dependencies
// Handler → Service → Repo
async fn get_order(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<Order>, ApiError> {
    let order = state.order_service.get_order(id).await?;
    Ok(Json(order))
}

Config Access Rules

Direct environment variable access is prohibited: Business code must not call std::env::var() directly. All configuration must go through the Config system.

// Bad: reading env vars directly
let api_key = std::env::var("OPENAI_API_KEY").unwrap();

// Good: reading through Config
let api_key = config.openai.api_key.clone();

Environment variable to Config mapping:

// src/infra/config.rs
#[derive(Debug, Clone, Deserialize)]
pub struct SomeConfig {
    #[serde(default = "default_some_value")]
    pub some_value: String,
}

fn default_some_value() -> String {
    std::env::var("SOME_ENV_VAR").unwrap_or_default()
}

Rationale:

  • Centralized configuration management for easier tracking and debugging
  • Supports multiple sources (TOML files, env vars, CLI args) with unified merging
  • Type-safe with startup-time validation
  • Enables mock config injection in tests

Performance Guidelines

Database Query Principles

  • Avoid N+1: Use JOINs or batch queries for associated data; never query inside a loop
  • Index wisely: Index columns used in WHERE/ORDER BY; mind column order in composite indexes
  • Pagination is mandatory: List queries must be paginated; never return unbounded results
  • EXPLAIN complex queries: Check execution plans for multi-table JOINs or subqueries

Caching Strategy

Use the Cache-Aside pattern as the default caching approach: check cache first, on miss query the database, then populate the cache. Choose cache location (Redis for shared/distributed state, in-process memory for read-heavy config data) and TTL based on data volatility and consistency requirements. Always implement explicit cache invalidation when the source data is mutated.

Install via CLI
npx skills add https://github.com/longzhi/rust-coding-standards --skill rust-architecture
Repository Details
star Stars 2
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator