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

LayerContainsRecipeBox example
PresentationUI + state holdersRecipeListActivity, RecipeListViewModel
DomainPlain Java use cases, no Android importsSaveRecipeUseCase, GetRecipesUseCase
DataRepository implementation + data sourcesRecipeRepository 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.

Presentation ──depends on──▶ Domain ◀──depends on── Data (Activity, ViewModel) (UseCase, plain Java) (Repository, Room, Retrofit) Domain depends on NOTHING outside itself.
Explanation: Because SaveRecipeUseCase never imports android.* or Room classes, it can be unit-tested with plain JUnit, with no emulator and no Android framework on the classpath.

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);
    }
}
Explanation:
  • 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
        }
    }
}
Explanation: The ViewModel doesn't know or care whether SaveRecipeUseCase saves to Room, Retrofit, both, or neither — it just calls execute(). That's the whole point of the layering.

Full Call Chain

AddRecipeActivity (Presentation) └─ AddRecipeViewModel.onSaveClicked() └─ SaveRecipeUseCase.execute() (Domain) └─ RecipeRepositoryImpl.save() (Data) ├─ RecipeDao.insert() (Room) └─ RecipeApiService.uploadRecipe() (Retrofit)

Is It Worth the Ceremony?

Honest note: Clean Architecture adds files, interfaces, and indirection that a small, one-screen app simply doesn't need — a single-Activity RecipeBox prototype is faster to build without it. It earns its keep as an app grows: more screens, more developers, and more business rules that need to be tested in isolation from Android itself. Don't reach for the full three-layer split on day one; introduce it when the pain of not having it becomes real.

Hands-On Practice

  1. Extract RecipeBox's recipe-saving logic out of an Activity into a SaveRecipeUseCase.
  2. Define RecipeRepository as an interface in the Domain layer.
  3. Write a plain JUnit test for SaveRecipeUseCase with a fake repository — no emulator required.
Clean Architecture describes where code should live. The next lesson, Dependency Injection (Dagger 2), covers how a ViewModel actually gets a fully-built SaveRecipeUseCase handed to it, instead of constructing one manually.