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 declarationsdomain.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, auditingrepo.rs— Infrastructure layer: database access, SQL explicitly visiblehandler.rs— Presentation layer: HTTP handlers, parameter parsing, response formattingdto.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.rsor inline inhandler.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.rsis pure Rust: no external dependencies, no database awarenessservice.rsis 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.rsandinfra/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.rsconcise - 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 inservice.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.