The interfaces layer defines trait contracts for data access without specifying implementation details. This enables dependency inversion and testability.
Repository Traits
Repository traits abstract data access operations, allowing infrastructure implementations to be swapped without affecting business logic.
UserRepository
Data access contract for user persistence operations.
Trait defining user data access operations. Requires Send + Sync for async safety.
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn create(&self, user: &User) -> Result<User, ApiError>;
async fn get_by_id(&self, id: &str) -> Result<Option<User>, ApiError>;
async fn get_by_email(&self, email: &str) -> Result<Option<User>, ApiError>;
async fn get_all(&self) -> Result<Vec<User>, ApiError>;
async fn get_paginated(&self, page: i32, per_page: i32) -> Result<(Vec<User>, i32), ApiError>;
async fn update(&self, user: &User) -> Result<(), ApiError>;
async fn delete(&self, id: &str) -> Result<bool, ApiError>;
async fn count(&self) -> Result<i32, ApiError>;
async fn exists_by_email(&self, email: &str) -> Result<bool, ApiError>;
}
Methods
create
async fn create(&self, user: &User) -> Result<User, ApiError>
Persists new user entity and returns the created user with any database-generated fields.
Reference to User entity to persist
Returns created User on success, DatabaseError on failure
get_by_id
async fn get_by_id(&self, id: &str) -> Result<Option<User>, ApiError>
Retrieves user by unique identifier.
result
Result<Option<User>, ApiError>
Returns Some(User) if found, None if not found, Error on database failure
get_by_email
async fn get_by_email(&self, email: &str) -> Result<Option<User>, ApiError>
Retrieves user by email address. Takes plain string instead of EmailAddress value object for flexibility.
Email address to search for
Example usage from source (auth_service.rs:33-35):
if self.user_repository.exists_by_email(&request.email).await? {
return Err(ApiError::Conflict("User already exists".to_string()));
}
get_all
async fn get_all(&self) -> Result<Vec<User>, ApiError>
Retrieves all users without pagination. Should be used sparingly for small datasets.
result
Result<Vec<User>, ApiError>
Returns vector of all users
get_paginated
async fn get_paginated(
&self,
page: i32,
per_page: i32
) -> Result<(Vec<User>, i32), ApiError>
Retrieves paginated users with total count for UI pagination.
result
Result<(Vec<User>, i32), ApiError>
Returns tuple of (users vector, total count)
Example usage from source (user_service.rs:53-56):
let (users, total) = self
.user_repository
.get_paginated(page, per_page)
.await?;
update
async fn update(&self, user: &User) -> Result<(), ApiError>
Updates existing user entity. Implementation should update all mutable fields.
User entity with updated values
Returns () on success, Error on failure
delete
async fn delete(&self, id: &str) -> Result<bool, ApiError>
Deletes user by ID. Returns boolean indicating whether deletion occurred.
Returns true if user was deleted, false if not found
Example usage from source (user_service.rs:164-168):
let deleted = self.user_repository.delete(user_id).await?;
if !deleted {
return Err(ApiError::NotFound("User not found".to_string()));
}
count
async fn count(&self) -> Result<i32, ApiError>
Returns total number of users in system.
exists_by_email
async fn exists_by_email(&self, email: &str) -> Result<bool, ApiError>
Efficient check for email existence without loading full entity.
Returns true if email exists, false otherwise
TestItemRepository
Data access contract for test item persistence operations.
Trait defining test item data access operations. Requires Send + Sync for async safety.
#[async_trait]
pub trait TestItemRepository: Send + Sync {
async fn create(&self, item: &TestItem) -> Result<TestItem, ApiError>;
async fn get_by_id(&self, id: &str) -> Result<Option<TestItem>, ApiError>;
async fn get_all(&self) -> Result<Vec<TestItem>, ApiError>;
async fn get_paginated(&self, page: i32, per_page: i32) -> Result<(Vec<TestItem>, i32), ApiError>;
async fn update(&self, item: &TestItem) -> Result<(), ApiError>;
async fn delete(&self, id: &str) -> Result<bool, ApiError>;
async fn count(&self) -> Result<i32, ApiError>;
}
Methods
create
async fn create(&self, item: &TestItem) -> Result<TestItem, ApiError>
Persists new test item and returns created entity.
Test item entity to create
result
Result<TestItem, ApiError>
Returns created TestItem on success
Example usage from source (test_item_service.rs:22-25):
let item = TestItem::new(request.subject, request.optional_field);
let created_item = self.repository.create(&item).await?;
Ok(created_item.to_response())
get_by_id
async fn get_by_id(&self, id: &str) -> Result<Option<TestItem>, ApiError>
Retrieves test item by ID.
result
Result<Option<TestItem>, ApiError>
Returns Some(TestItem) if found, None otherwise
get_all
async fn get_all(&self) -> Result<Vec<TestItem>, ApiError>
Retrieves all test items.
get_paginated
async fn get_paginated(
&self,
page: i32,
per_page: i32
) -> Result<(Vec<TestItem>, i32), ApiError>
Retrieves paginated test items with total count.
result
Result<(Vec<TestItem>, i32), ApiError>
Returns tuple of (items, total_count)
Example usage from source (test_item_service.rs:40-43):
let (items, total) = self.repository.get_paginated(page, per_page).await?;
let item_responses: Vec<TestItemResponse> = items.into_iter().map(|i| i.to_response()).collect();
Ok(PaginatedTestItemsResponse::new(item_responses, total, page, per_page))
update
async fn update(&self, item: &TestItem) -> Result<(), ApiError>
Updates existing test item.
Test item with updated fields
delete
async fn delete(&self, id: &str) -> Result<bool, ApiError>
Deletes test item by ID.
Returns true if deleted, false if not found
Example usage from source (test_item_service.rs:64-69):
let deleted = self.repository.delete(id).await?;
if !deleted {
return Err(ApiError::NotFound("Test item not found".to_string()));
}
Ok(())
count
async fn count(&self) -> Result<i32, ApiError>
Returns total number of test items.
Design Principles
Dependency Inversion
Traits allow high-level application logic to depend on abstractions rather than concrete implementations. Infrastructure depends on interfaces, not vice versa.
Testability
Traits enable mock implementations for testing:
struct MockUserRepository {
users: Mutex<Vec<User>>,
}
#[async_trait]
impl UserRepository for MockUserRepository {
async fn create(&self, user: &User) -> Result<User, ApiError> {
self.users.lock().unwrap().push(user.clone());
Ok(user.clone())
}
// ...
}
Async Trait
All repository methods are async and use the async_trait macro for ergonomic async trait definitions.
Error Handling
All methods return Result<T, ApiError> for consistent error handling across layers.
Send + Sync Bounds
Traits require Send + Sync to ensure implementations are safe for concurrent async access.
Implementation Notes
String vs Value Objects
Repository methods take plain &str for lookup parameters (like email) rather than value objects. This provides flexibility and avoids forcing callers to construct value objects for queries.
Option Return Types
Getter methods return Option<T> to distinguish between “not found” (None) and database errors (Err).
Boolean Delete Returns
Delete methods return bool to indicate whether the entity existed, allowing callers to handle not-found cases.
Paginated queries return (Vec<T>, i32) tuples containing both results and total count for building pagination UIs.