Add a Use Case
Guide for adding a new use case following the file-per-use-case pattern.
Goal
Add a new use case in the application layer that orchestrates domain logic and depends on a repository trait, with unit tests using a mock repository.
Prerequisites
- A domain entity or value object the use case operates on (see
crates/domain/src/) - Familiarity with the repository trait pattern and the application layer structure
Steps
1. Define the repository trait and error types
If this is the first use case in a bounded context, create services.rs in your context directory. Existing contexts:
crates/application/src/bookmark/services.rs.
use domain::Bookmark;use uuid::Uuid;
#[derive(Debug, thiserror::Error)]pub enum BookmarkRepositoryError { #[error("Bookmark not found: {0}")] NotFound(String),
#[error("Invalid bookmark data: {0}")] InvalidData(String),
#[error("Database error: {0}")] Database(String),}
pub type BookmarkResult<T> = Result<T, BookmarkRepositoryError>;
/// Abstraction for bookmark persistence operations.////// # Mutation methods////// No mutation methods have default implementations. This is intentional —/// a missing override would silently no-op, causing data loss bugs.pub trait BookmarkRepository: Send + Sync { fn create(&self, bookmark: &Bookmark) -> BookmarkResult<()>; fn get(&self, id: Uuid) -> BookmarkResult<Bookmark>; fn list(&self) -> BookmarkResult<Vec<Bookmark>>; fn delete(&self, id: Uuid) -> BookmarkResult<()>;}Mutation methods must not have default implementations. A Ok(()) default on a create or delete method causes
silent data loss when the implementation is missing — the caller sees success but nothing was persisted. No default
forces a compile error.
2. Create the use case file
Create a new file named after the use case: crates/application/src/<context>/create_<entity>.rs.
use super::services::{BookmarkRepository, BookmarkRepositoryError, BookmarkResult};use domain::Bookmark;
/// Request to create a new bookmark.#[derive(Debug)]pub struct CreateBookmarkRequest { pub name: String, pub description: Option<String>, pub created_by: Option<String>,}
/// Use case for creating a named bookmark in the event timeline.pub struct CreateBookmarkUseCase<R: BookmarkRepository> { repository: R,}
impl<R: BookmarkRepository> CreateBookmarkUseCase<R> { pub fn new(repository: R) -> Self { Self { repository } }
#[tracing::instrument(skip(self, request), level = "debug")] pub fn execute(&self, request: CreateBookmarkRequest) -> BookmarkResult<Bookmark> { let bookmark = Bookmark::new(request.name, request.description, request.created_by) .map_err(|e| BookmarkRepositoryError::InvalidData(e.to_string()))?;
self.repository.create(&bookmark)?;
tracing::debug!(id = %bookmark.id, name = %bookmark.name, "bookmark_created");
Ok(bookmark) }}Conventions:
- Generic over the repository trait (
R: BookmarkRepository) — keeps the use case testable with mocks. - Request struct instead of individual arguments — easier to extend, documents intent.
- Domain validation errors convert to
InvalidDataat the use case boundary. #[tracing::instrument]skipsselfandrequestto avoid logging sensitive data.
3. Add the use case to mod.rs
Re-export from the context’s mod.rs:
mod create_bookmark;mod delete_bookmark;mod get_bookmark;mod list_bookmarks;pub(crate) mod services;
#[cfg(test)]pub(crate) mod test_helpers;
pub use create_bookmark::{CreateBookmarkRequest, CreateBookmarkUseCase};pub use delete_bookmark::DeleteBookmarkUseCase;pub use get_bookmark::GetBookmarkUseCase;pub use list_bookmarks::ListBookmarksUseCase;pub use services::{BookmarkRepository, BookmarkRepositoryError, BookmarkResult};If the context module is new, declare it in crates/application/src/lib.rs:
pub mod bookmark;pub use bookmark::{CreateBookmarkRequest, CreateBookmarkUseCase, /* ... */};4. Create the mock repository for tests
Add test_helpers.rs gated behind #[cfg(test)]:
use super::services::{BookmarkRepository, BookmarkRepositoryError, BookmarkResult};use domain::Bookmark;use std::collections::HashMap;use std::sync::{Arc, Mutex};use uuid::Uuid;
#[derive(Clone, Default)]pub struct MockBookmarkRepository { pub bookmarks: Arc<Mutex<HashMap<Uuid, Bookmark>>>,}
impl MockBookmarkRepository { pub fn new() -> Self { Self::default() }}
impl BookmarkRepository for MockBookmarkRepository { fn create(&self, bookmark: &Bookmark) -> BookmarkResult<()> { self.bookmarks.lock().unwrap().insert(bookmark.id, bookmark.clone()); Ok(()) }
fn get(&self, id: Uuid) -> BookmarkResult<Bookmark> { self.bookmarks .lock() .unwrap() .get(&id) .cloned() .ok_or_else(|| BookmarkRepositoryError::NotFound(id.to_string())) }
fn list(&self) -> BookmarkResult<Vec<Bookmark>> { Ok(self.bookmarks.lock().unwrap().values().cloned().collect()) }
fn delete(&self, id: Uuid) -> BookmarkResult<()> { self.bookmarks .lock() .unwrap() .remove(&id) .ok_or_else(|| BookmarkRepositoryError::NotFound(id.to_string()))?; Ok(()) }}5. Write tests in the use case file
Add a #[cfg(test)] mod tests block at the bottom of the use case file. Test naming convention:
test_[what]_[condition]_[expected].
#[cfg(test)]mod tests { use super::*; use crate::bookmark::test_helpers::MockBookmarkRepository;
#[test] fn test_create_bookmark_success() { let repo = MockBookmarkRepository::new(); let use_case = CreateBookmarkUseCase::new(repo.clone());
let result = use_case.execute(CreateBookmarkRequest { name: "Before major refactor".to_string(), description: Some("Save point before big changes".to_string()), created_by: None, });
assert!(result.is_ok()); let bm = result.unwrap(); assert_eq!(bm.name, "Before major refactor");
let bookmarks = repo.bookmarks.lock().unwrap(); assert!(bookmarks.contains_key(&bm.id)); }
#[test] fn test_create_bookmark_empty_name_fails() { let repo = MockBookmarkRepository::new(); let use_case = CreateBookmarkUseCase::new(repo);
let result = use_case.execute(CreateBookmarkRequest { name: "".to_string(), description: None, created_by: None, });
assert!(result.is_err()); }}Test what the use case orchestrates: that the domain validates inputs, that the repository is called with the right data, and that errors propagate correctly. Do not test repository internals here.
Verification
cargo test -p application— all use case tests passcargo clippy -p application— no lint warnings- The use case is exported from
crates/application/src/lib.rsand importable by the commands layer
See Also
- Adding a Tauri Command — wire the use case to the frontend
- Development Guide: File-Per-Use-Case Pattern
- Domain Rules — invariants the use case must enforce
Was this page helpful?
Thanks for your feedback!