On this page
The Domain Layer contains full bounded contexts with business logic, repositories, and services. Each domain crate owns its database tables and defines its public API.
Characteristics
- Contains business logic - Domain rules and workflows
- Owns database tables - SQL schemas in the crate
- Repository pattern - Data access abstraction
- Event-driven - Communicates via events
- Clear public API - Well-defined boundaries
Crates
| Crate | Purpose |
|---|---|
| systemprompt-users | User management, roles, permissions |
| systemprompt-oauth | OAuth2/OIDC flows, token management |
| systemprompt-files | File storage, metadata, serving |
| systemprompt-analytics | Metrics, usage tracking, reports |
| systemprompt-content | Markdown, publishing, search |
| systemprompt-mcp | MCP server registry, tool execution |
| systemprompt-ai | LLM providers, request logging |
| systemprompt-agent | A2A protocol, agent registry |
| systemprompt-templates | Template registry, rendering |
Required Structure
Every domain crate follows this structure:
domain/{name}/
├── Cargo.toml
├── schema/ # SQL files
│ ├── {table}.sql
│ └── migrations/
├── src/
│ ├── lib.rs # Public API
│ ├── extension.rs # Extension registration
│ ├── models/ # Domain types
│ │ ├── mod.rs
│ │ └── {entity}.rs
│ ├── repository/ # Data access
│ │ ├── mod.rs
│ │ └── {entity}_repository.rs
│ └── services/ # Business logic
│ ├── mod.rs
│ └── {service}.rs
Dependency Rules
Domain layer crates:
- Can depend on Shared layer crates
- Can depend on Infrastructure layer crates
- Can depend on other Domain crates (with care)
- Cannot depend on Application or Entry crates
Cross-Domain Communication
Domain crates should communicate via events, not direct dependencies:
// In users domain
events.emit(UserCreated { user_id, email }).await;
// In analytics domain (subscribes to user events)
events.subscribe::<UserCreated, _>(|event| async {
analytics.track_signup(event.user_id).await;
});
Common Patterns
Repository Pattern
// Define the trait
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: UserId) -> Result<Option<User>>;
async fn find_by_email(&self, email: &str) -> Result<Option<User>>;
async fn save(&self, user: &User) -> Result<()>;
async fn delete(&self, id: UserId) -> Result<()>;
}
// Implementation
pub struct PgUserRepository {
pool: Arc<PgPool>,
}
impl UserRepository for PgUserRepository {
async fn find_by_id(&self, id: UserId) -> Result<Option<User>> {
sqlx::query_as!(User,
r#"SELECT * FROM users WHERE id = $1"#,
id.as_uuid()
)
.fetch_optional(&*self.pool)
.await
.map_err(Into::into)
}
}
Service Layer
pub struct UserService {
repository: Arc<dyn UserRepository>,
events: Arc<EventBus>,
}
impl UserService {
pub async fn create_user(&self, input: CreateUserInput) -> Result<User> {
// Validation
input.validate()?;
// Check uniqueness
if self.repository.find_by_email(&input.email).await?.is_some() {
return Err(UserError::EmailExists);
}
// Create user
let user = User::new(input);
self.repository.save(&user).await?;
// Emit event
self.events.emit(UserCreated {
user_id: user.id,
email: user.email.clone(),
}).await;
Ok(user)
}
}
Layer Diagram
┌────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ │
│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────────┐ │
│ │ users │ │ oauth │ │ files │ │ analytics │ │
│ └───────┘ └───────┘ └───────┘ └───────────┘ │
│ │
│ ┌─────────┐ ┌─────┐ ┌────┐ ┌───────┐ ┌───────────┐ │
│ │ content │ │ mcp │ │ ai │ │ agent │ │ templates │ │
│ └─────────┘ └─────┘ └────┘ └───────┘ └───────────┘ │
│ │
│ ↓ │
│ Depends on INFRASTRUCTURE and SHARED │
└────────────────────────────────────────────────────────────┘