Clean Architecture (Java)
Clean Architecture is a way of organizing a codebase into layers, so that business rules don't depend on frameworks, UI toolkits, or databases. For RecipeBox, that means the logic that decides "how a recipe is saved" shouldn't care whether it's an Activity, a Room database, or a Retrofit call doing the asking.
The Three Layers
Presentation
Activity/Fragment + ViewModel
Domain
UseCase + business rules
Data
Repository + Room/Retrofit
| Layer | Contains | RecipeBox example |
|---|---|---|
| Presentation | UI + state holders | RecipeListActivity, RecipeListViewModel |
| Domain | Plain Java use cases, no Android imports | SaveRecipeUseCase, GetRecipesUseCase |
| Data | Repository implementation + data sources | RecipeRepository wrapping Room and Retrofit |
The Dependency Direction Rule
The rule that gives Clean Architecture its name: dependencies only point inward. The Domain layer knows nothing about Android, Room, or Retrofit. The Presentation and Data layers depend on Domain — never the other way around.
Domain Layer: A UseCase
A UseCase (sometimes called an Interactor) wraps one specific piece of business logic — a single action the app can perform — behind a small, testable class.
// domain/SaveRecipeUseCase.java — no Android imports here
public class SaveRecipeUseCase {
private final RecipeRepository repository;
public SaveRecipeUseCase(RecipeRepository repository) {
this.repository = repository;
}
public void execute(Recipe recipe) {
if (recipe.title == null || recipe.title.trim().isEmpty()) {
throw new IllegalArgumentException("Recipe title cannot be empty");
}
repository.save(recipe);
}
}
- RecipeRepository here is an interface defined in the Domain layer — the Data layer provides the actual implementation.
- Validation ("title cannot be empty") lives here, not scattered across every Activity that happens to save a recipe.
Data Layer: Implementing the Repository
// data/RecipeRepositoryImpl.java
public class RecipeRepositoryImpl implements RecipeRepository {
private final RecipeDao dao; // Room
private final RecipeApiService api; // Retrofit
public RecipeRepositoryImpl(RecipeDao dao, RecipeApiService api) {
this.dao = dao;
this.api = api;
}
@Override
public void save(Recipe recipe) {
dao.insert(recipe);
api.uploadRecipe(recipe).enqueue(new RecipeUploadCallback());
}
}
Presentation Layer: Calling the UseCase
// presentation/AddRecipeViewModel.java
public class AddRecipeViewModel extends ViewModel {
private final SaveRecipeUseCase saveRecipeUseCase;
public AddRecipeViewModel(SaveRecipeUseCase saveRecipeUseCase) {
this.saveRecipeUseCase = saveRecipeUseCase;
}
public void onSaveClicked(Recipe recipe) {
try {
saveRecipeUseCase.execute(recipe);
} catch (IllegalArgumentException e) {
// expose an error LiveData to the Activity — see ViewModel & LiveData
}
}
}
Full Call Chain
Is It Worth the Ceremony?
Hands-On Practice
- Extract RecipeBox's recipe-saving logic out of an Activity into a SaveRecipeUseCase.
- Define RecipeRepository as an interface in the Domain layer.
- Write a plain JUnit test for SaveRecipeUseCase with a fake repository — no emulator required.