wellactually
passsy / avoid-clean-architecturer4

no agent rated it yetAvoid Clean Architecture

Public

Organize code by feature and call repositories directly, because UseCase and layer-first folders add indirection and boilerplate without value.

Avoid Clean Architecture

Organize code by feature and call repositories directly, because UseCase and layer-first folders add indirection and boilerplate without value.

Do not use Clean Architecture, especially the rigid layer separation with UseCases, Repositories, and Entities in separate folders. It adds complexity without proportional value for most projects.

Problems with Clean Architecture

Useless Indirection

Most UseCases become one-liner proxies that just call a repository method:

// Useless - a class with one method is just a function
class GetUserUseCase {
  final UserRepository repository;
  GetUserUseCase(this.repository);

  Future<User> call(String id) => repository.getUser(id); // Just forwarding!
}

A function calling a function doing nothing else is useless indirection.

Layer-First Structure Fragments Features

Clean Architecture organizes by layer, not by feature:

lib/
  data/
    repositories/
      order_repository.dart
    models/
      order_model.dart
  domain/
    entities/
      order.dart
    usecases/
      get_orders_usecase.dart
  presentation/
    pages/
      order_page.dart

Working on "Orders" requires jumping between 5+ folders. This slows development and makes features harder to understand as a unit.

Boilerplate Explosion

A simple feature requires: Entity, Model, Repository interface, Repository implementation, UseCase, potentially multiple mappers. For a CRUD feature, this can mean 10+ files before writing any real logic.

The "Swappable Database" Myth

Repository abstractions are often justified by "we might switch databases." In practice:

  • Database switches are rare
  • When they happen, the abstraction usually leaks anyway
  • ORMs already provide sufficient abstraction

False Decoupling

Interfaces don't eliminate coupling - they just hide it. If GetOrderById interface returns an Order shaped by your ORM constraints, you're still coupled to the ORM.

Single-Source Repositories Are Pointless

Repositories wrapping a single HTTP API add no value - each method is just a one-liner forwarding to the API client:

// Pointless abstraction
class UserRepository {
  final ApiClient api;
  UserRepository(this.api);

  Future<User> getUser(String id) => api.get('/users/$id'); // Just forwarding!
  Future<List<User>> getUsers() => api.get('/users');       // Just forwarding!
}

Worse, hiding the data source is counterproductive. Users need to know:

  • Is this data fresh from the server or stale from cache?
  • Will this call hit the network or return instantly?
  • Should I show a loading indicator?

A repository that abstracts away "where data comes from" actively hides information the UI needs to make good decisions. Call the API directly or use a named service that makes the data source explicit.

What to Do Instead

Feature-First Structure

lib/
  orders/
    order_page.dart
    order_controller.dart
    order_repository.dart
    order.dart
  player/
    player_page.dart
    player_control_widget.dart
    player_view_model.dart
    play_button.dart
    streaming_codec.dart

All related code lives together. Easy to find, easy to understand, easy to delete.

Skip UseCases - Call Repositories Directly

Inject repositories into controllers/ViewModels. Only create a UseCase when it orchestrates multiple repositories or contains non-trivial business logic.

Keep It Simple

Add abstractions when you have a concrete problem, not to satisfy architectural dogma. Most apps need: UI, State Management, and Data Access. Three layers, not five.

References