Sparround

The over-engineering boundary: when to simplify

Throughout this branch we showed the benefits of layers. This topic looks at the other side — their cost — because every abstraction charges a fee.

The official Flutter recommendations themselves strike this balance: some items are top priority (data/UI separation, repositories, MVVM, DI, immutable models, testing with fakes), while others are conditional (the domain layer, separate API/domain models, the specific state management choice).

The conditions on the conditional items are concrete:

  • The domain layer (use cases) — only when complex logic crowds the view models. The guide considers it overhead in most apps.
  • Separate API and domain models — recommended in large apps, extra verbosity in small ones.

The practical consequence: the "full set" architecture is not the right answer for every project. In a small project, the minimum set (service plus repository plus notifier) fully satisfies the highest-priority part of the official recommendations.

AbstractionIts costWhen it paysWhen it does not
A repository contract (abstract)One extra fileAlways — the second implementation is the test fakeIn a throwaway prototype with no tests
A separate DTO plus mapperTwo files plus a mapper test per modelAnother team owns the backend; the JSON is dirtyYou own the backend and the shape already matches
Use case classesFour artifacts: file, provider, mock, testTwo repositories combine, or the logic repeatsWrapping a single repository call
`Result` / `Either`A `switch` at every callWrites and multi-step operationsSimple reads — `AsyncNotifier` suffices
A `sealed` state unionN variants times M fieldsForms, wizards, mutually exclusive statesA simple list — `AsyncValue` suffices
Splitting into packages`pubspec` management, codegen per package, `melos`Several teams, shared code, a long lifetimeOne developer, 15 screens
`freezed` for every model`build_runner` timeJSON, unions, many fieldsA two-field model — `Equatable` suffices

Symptoms of over-engineering. These are concrete and observable:

  • One-line classes. class GetProductsUseCase { call() => _repo.fetchActive(); } — no logic, only forwarding.
  • An abstraction with a single user that is never even swapped in tests. An interface exists, one class implements it, and it will never change.
  • Navigation depth. Chasing one bug means opening six files: widget → notifier → use case → repository → mapper → service.
  • Empty folders. usecases/, entities/, datasources/ — inherited from a template and never used.
  • Tests that are only mock setup. Twenty lines of when(…) and one expect — such a test verifies no behaviour.
  • Your own "framework". Generic bases written for one feature: BaseRepository<T>, BaseNotifier<S, E>.

The other side: symptoms of under-engineering. You need these too, to see the balance:

  • Tests force you to mock HTTP or JSON.
  • The same calculation repeats in three or more places.
  • One backend field rename breaks five or more files.
  • A build method is 200 lines long.
  • The user sees a SocketException message.

The check question. For each abstraction: "if I delete this, what gets worse?" The answer must be concrete ("I won't be able to replace the network in tests", "the second screen will copy the code"). If the answer is "it won't match clean architecture" — the abstraction has lost its reason.

text
╔═══ PROTOTİP (bir dəfəlik, 1-2 həftə ömür) ═══╗
✅ Widget + birbaşa sorğu — kifayətdir
❌ Repository, DTO, use-case, test — vaxt itkisi
Səbəb: kod silinəcək. Abstraksiyanın qazancı yaşamağa vaxt tapmır.

╔═══ KİÇİK (< 10 ekran, 1 developer, uzun ömür) ═══╗
✅ Service + repository (müqavilə ilə) + notifier
✅ Domain modelləri (immutable, biznes getter-ləri)
✅ Tipli Failure + dörd UI halı
✅ Repository və notifier testləri
⚠️ DTO: yalnız "çirkli" endpoint-lər üçün
❌ Use-case sinifləri (şərtlər ödənmir)
❌ Paketlərə bölmə
Qeyd: bu dəst rəsmi tövsiyələrin ƏN YÜKSƏK prioritetli
hissəsini tam ödəyir. "Yarımçıq" deyil.

╔═══ ORTA (10-40 ekran, 2-5 developer) ═══╗
✅ Yuxarıdakıların hamısı
✅ DTO + mapper (bütün endpoint-lər)
✅ Hibrid qovluq strukturu (ui/<feature>, data/ qat üzrə)
✅ Result (yazma əməliyyatları üçün)
✅ CI-də domain təmizliyi yoxlaması
⚠️ Use-case: yalnız şərt ödənən yerlərdə (3-5 ədəd normaldır)
❌ Paketlərə bölmə (hələ lazım deyil)

╔═══ BÖYÜK (40+ ekran, bir neçə komanda) ═══╗
✅ Yuxarıdakıların hamısı
✅ Use-case qatı (daha geniş)
✅ Paketlərə bölmə: domain, data, feature-lər
✅ melos + paket-səviyyəsində CI
✅ Golden testlər, integration test paketi

─── QAYDA ───
Dəsti YUXARIDAN AŞAĞI böyütmək lazımdır, əksinə deyil.
Kiçik layihəni "böyük" dəstlə başlamaq ən çox rast gəlinən
over-engineering formasıdır — və onu geri qaytarmaq çətindir.

The recommended set by project size. Each line follows the priorities in the official recommendations.

In an interview this topic is a strong signal. "I apply clean architecture everywhere" is a weak answer; "which parts I apply and why" is a strong one. Referring to the official recommendations' priority split grounds that answer in a source: "I don't write a domain layer per feature, because the official guide treats it as conditional and our view models are still simple".

Practice. Write an audit for your own project (or a familiar one): list every abstraction and answer, in one sentence each, "if I delete this, what gets worse?"

Mark the abstractions whose answer is not concrete (only "it wouldn't match the principle") — those are deletion candidates.

Done means: the list holds at least eight abstractions, each with a concrete answer, and one or two candidates found (or a justified conclusion that all of them have a reason).

📚 Sources and documentation