This project uses a slimmed-down version of my Clean Architecture app, simplified to keep scope fair against the spaghetti counterpart. For the full-featured app, visit flutter-clean-arch-riverpod.
This is my undergraduate thesis (Systems Analysis and Development, UNISINOS, Brazil, 2026), advised by Prof. Dr. Margrit Reni Krug and graded with distinction.
- 📄 Full thesis (Portuguese):
docs/tcc_final.pdf - 🖥️ Slides: English · Portuguese
Build two Flutter apps with exactly the same functionality, but opposite architectural approaches: one following Clean Architecture + DDD, the other deliberately written as spaghetti code — no layers, no boundaries. Same features, same dependencies, same product. The only variable left is how the code is organized.
With that controlled pair in place, the thesis runs an actual analysis on top of it — static metrics, coupling, testability, a case study walking one feature change through both codebases — instead of just arguing in the abstract that "Clean Architecture is better."
A crypto quote tracker consuming the public Binance market data API:
- Live quotes list, with filtering by symbol
- Detail screen with a candlestick price chart (
fl_chart) - Favorites
- Offline cache fallback when there's no connection
- Preferences: language (EN/ES/PT), theme, locally persisted settings
Same screens, same dependencies (dio, shared_preferences, auto_route, the provider
package for state management), same product — the only independent variable is how the code is
organized.
Both apps render this identically — the UI is a controlled variable, not part of the comparison.
apps/clean |
apps/spaghetti |
|
|---|---|---|
| Structure | domain → application → data/infrastructure → presentation, each behind explicit contracts |
Flat: screens/, notifiers/, models/ — UI, state, HTTP, and persistence calls freely mixed |
| Dependency direction | Inward, towards domain (Dependency Rule) |
None enforced — any file can import anything |
flowchart TB
subgraph Clean["apps/clean"]
direction TB
P1["presentation\nscreens · providers"] --> A1["application\nuse cases"]
A1 --> DO1["domain\nentities · repository interfaces"]
D1["data\nrepositories_impl · mappers"] --> DO1
I1["infrastructure\nDio · SharedPreferences"] --> D1
end
subgraph Spaghetti["apps/spaghetti"]
direction TB
S1["screens"] --- N1["notifiers"]
N1 --- M1["models"]
S1 -. "direct HTTP + storage calls" .- N1
end
apps/clean here is deliberately simplified so its scope matches apps/spaghetti — see the
link at the top for the fuller reference version, with patterns like a Result-based error
model.
Numbers as reported in the thesis (Section 5, "Resultados") — static analysis of both lib/
trees with dart_code_linter plus a small import-coupling
script; see metrics/ for the raw data and how to reproduce it.
| Metric | Clean | Spaghetti |
|---|---|---|
Files (lib/) |
67 | 22 |
| Lines of code | 3,132 | 1,588 |
| Avg. lines of code per file | 46.7 | 72.2 |
| Avg. cyclomatic complexity per function | 1.51 | 1.72 |
| External-package imports (total / avg per file) | 19 / 0.34 | 22 / 0.96 |
| Test files | 41 | 1 (unmodified template) |
| Line coverage of tested code | 100% (385/385) | — |
Three numbers matter most here. Coupling: in the spaghetti app, files reach directly into
third-party packages (dio, shared_preferences, …) roughly three times more often per file,
because nothing forces that access behind an interface. Testability: the clean app has 41
test files with full coverage of the code under test, and getting there was straightforward —
each layer sits behind a narrow interface, so a test mocks one boundary (a repository, a data
source) instead of dragging in the rest of the app; the spaghetti app has zero real tests — not
because testing spaghetti code is impossible, but because nothing is isolated enough to test
without standing up the whole app around it. Complexity: average cyclomatic complexity per
function is ~12% lower on the clean side, and where complexity does concentrate, it's isolated
to the data/infrastructure layers (avg. CC 1.97 / 1.77 — mostly the Binance API response
parsing) rather than spread across the app; in the spaghetti app it concentrates in the
notifiers, which also hold UI state, business logic, and direct HTTP/storage calls.
None of this is free. The clean app has ~3× the files and ~2× the lines of code — a real upfront cost (interfaces, use cases, repositories) that the spaghetti app simply doesn't pay. For a small app with no growth or testing requirements, that cost may not be worth it.
- Clean Architecture doesn't make a small app's inherent complexity disappear — it redistributes and organizes that complexity into isolated, individually simple pieces, regardless of the app's size.
- The payoff is in predictability of maintenance cost over time. In the spaghetti app, cost compounds with every added feature; in the clean app, changes stay local to a layer.
- Clean Architecture isn't a competitor to MVC/MVVM — it's a complementary structuring principle that those patterns can sit inside of.
- It's justified for apps with room to grow, real testing requirements, and non-trivial business rules — not for prototypes, throwaway PoCs, or apps with no planned evolution.
- 100% test coverage was easy to reach on the clean side, and that wasn't a goal chased for its
own sake — it fell out of the architecture. Once business logic is isolated from
dioandshared_preferencesbehind interfaces, there's nothing left un-mockable to skip.
Both are standard Flutter projects (SDK ^3.11.4, Flutter 3.44.5 via FVM).
cd apps/clean # or apps/spaghetti
make fvm # installs FVM and pins the Flutter version for this app
flutter pub get
flutter runEach app also has a Makefile (make apk, make flutter-test, …) — see the app-level
apps/clean/README.md for the full architecture breakdown and folder
structure.
.
├── apps/
│ ├── clean/ # Clean Architecture + DDD implementation
│ └── spaghetti/ # Same app, unlayered
├── metrics/ # Static analysis + coupling data behind the comparison, with README
└── docs/ # Full thesis (PDF, Portuguese), defense slides (EN & PT), screenshots
MIT — feel free to use either codebase as a reference or starting point.




