Skip to content
Documentation GitHub
Development Guides

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.

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.

crates/application/src/bookmark/create_bookmark.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 InvalidData at the use case boundary.
  • #[tracing::instrument] skips self and request to avoid logging sensitive data.

3. Add the use case to mod.rs

Re-export from the context’s mod.rs:

crates/application/src/bookmark/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)]:

crates/application/src/bookmark/test_helpers.rs
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 pass
  • cargo clippy -p application — no lint warnings
  • The use case is exported from crates/application/src/lib.rs and importable by the commands layer

See Also

Was this page helpful?