Backend DDD Implementation Guide
Workflow 1: Add Feature to Existing Module
When: Adding new domain operation to existing bounded context
Steps:
- •
Define domain types (
types.rs):- •Add entity, value object, or enum
- •Include validation in constructors
- •
Add infrastructure (
repository.rsormapper.rs):- •Add persistence or protocol methods
- •Keep separate from domain logic
- •
Expose via aggregate root (
mod.rs):- •Add public method coordinating the operation
- •Use repository/mapper internally
- •
Create Tauri command (
commands.rs):- •Add
#[tauri::command]function - •Call aggregate root method
- •Handle errors, return serializable result
- •Add
- •
Register command (
lib.rs):- •Add to
invoke_handler![...]list
- •Add to
Workflow 2: Create New Bounded Context
When: Adding major new feature (e.g., setlists, profiles)
Steps:
- •
Create module directory:
bashmkdir -p tauri/src/{domain} cd tauri/src/{domain} - •
Create
types.rs:rust// Define main entity (aggregate root data) pub struct EntityState { /* ... */ } // Define value objects with validation pub struct ValueObject(String); impl ValueObject { pub fn new(val: String) -> Result<Self, Error> { /* validate */ } } // Define domain enums pub enum DomainConcept { VariantA, VariantB } - •
Create infrastructure file (
repository.rsormapper.rs):rust// repository.rs example pub struct Repository { conn: Arc<Mutex<rusqlite::Connection>>, } impl Repository { pub fn save(&self, entity: &Entity) -> Result<()> { /* ... */ } pub fn find_by_id(&self, id: &Id) -> Result<Option<Entity>> { /* ... */ } } - •
Create
mod.rs(aggregate root):rustmod types; mod repository; pub use types::*; pub struct AggregateRoot { repository: Arc<Repository>, } impl AggregateRoot { pub fn new(deps: Dependencies) -> Result<Self> { /* ... */ } pub fn do_something(&mut self) -> Result<()> { /* ... */ } } - •
Register module in
tauri/src/lib.rs:rustpub mod {domain}; - •
Add Tauri commands in
commands.rs:rust#[tauri::command] async fn command_name( state: State<'_, SharedAggregateRoot>, ) -> Result<ResponseType, String> { state.do_something() .await .map_err(|e| e.to_string()) }
Workflow 3: Add Database Table
When: New entity needs persistence
Steps:
- •
Design schema:
sqlCREATE TABLE entity_name ( id TEXT PRIMARY KEY, field TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX idx_field ON entity_name(field); - •
Add migration (in
repository.rs):rustfn init_schema(conn: &Connection) -> Result<()> { conn.execute( "CREATE TABLE IF NOT EXISTS ...", [], )?; Ok(()) } - •
Implement repository methods:
rustimpl Repository { pub fn new(db_path: PathBuf) -> Result<Self> { let conn = Connection::open(db_path)?; Self::init_schema(&conn)?; Ok(Self { conn: Arc::new(Mutex::new(conn)) }) } } - •
Use Tauri
app_data_dir()for database path
Common Patterns
Value Object Constructor
pub struct BankNumber(u8);
impl BankNumber {
pub fn new(value: u8) -> Result<Self, ValidationError> {
if (45..=60).contains(&value) {
Ok(Self(value))
} else {
Err(ValidationError::InvalidRange { value, min: 45, max: 60 })
}
}
pub fn value(&self) -> u8 {
self.0
}
}
Async Repository Method
pub async fn save(&self, entity: &Entity) -> Result<()> {
let conn = self.conn.clone();
let entity = entity.clone();
tokio::task::spawn_blocking(move || {
let conn = conn.lock().unwrap();
conn.execute(
"INSERT INTO table (id, data) VALUES (?1, ?2)",
params![entity.id, serde_json::to_string(&entity)?],
)?;
Ok(())
}).await?
}
Error Conversion for Tauri
#[tauri::command]
async fn do_something(
state: State<'_, SharedService>,
) -> Result<Response, String> {
state.do_something()
.await
.map_err(|e| e.to_string())
}
Checklist
Before committing backend feature:
- • Domain types in
types.rswith validation - • Infrastructure in
repository.rsormapper.rs - • Aggregate root methods in
mod.rs - • Tauri commands in
commands.rs - • Commands registered in
lib.rs - • Error types use
thiserror - • Serde derives on types that cross IPC boundary
- • Async for I/O operations
- •
Result<T, E>for all fallible operations