At a glance#
A student app built once to last.
- 7+ years maintained, from Flutter 1.x through the latest stable
- 70+ feature modules in a single codebase
- 3 app stores — Google Play, App Store, and Amazon Appstore
- Solo-owned for six years, then a team
- Zero major production incidents across the whole window
- One Clean Architecture refactor, made in year two — still in place
This is the project I point to when someone asks what “senior” means in mobile engineering. Not the flashiest build. The one that stays up.
Context#
The NCC App is the student-assistance app for Northwest Career College — a private career college in Las Vegas running programs across healthcare, trades, and business. Students use it for class schedules, academic advising, attendance, payments, financial aid, campus events, announcements, the campus map, and the dozen-odd services a modern campus runs on. It’s the app a student opens on the first day of class and keeps on their home screen through graduation.
It ships on Android (Google Play), iOS (App Store), and Amazon Appstore. Three stores, three review processes, three device matrices. That detail shapes everything that follows — you don’t ship to three stores by hand for seven years and stay sane.
The success metric here isn’t download counts. It’s that every student relies on the app daily, and it has to just work.
From Provider to Clean Architecture#
The app didn’t start clean. It started fast. The first version was Flutter with the Provider package, and features shipped quickly — which was the right call at the time. But by year two it was clear the codebase wouldn’t survive the distance. State lived wherever I’d last put it. Features reached into each other’s internals. Adding the tenth feature meant touching the first nine.
The real risk was never features. It was maintainability.
So in year two I made the call to rebuild the app to Clean Architecture — mid-flight, without stopping the release cadence. Not a rewrite on a dark branch for six months. A migration: one feature at a time, layered passes, the app shippable every sprint. The most tangled features got pulled behind proper boundaries first; the rest followed.
The payoff is the whole point of this article: the same architecture has held for more than five years since, absorbing Dart null safety, Material 3, the Impeller rendering backend, and Dart 3 records and patterns — none of them as rewrites, all of them as contained migrations.
The framework changes. The app’s job doesn’t. Keeping a seam between the two is what let one solo engineer hold this together for seven years.
Flutter Clean Architecture, Under the Hood#
This is the part engineers care about: the actual shape of the codebase, and why each choice earned its place. The pattern is feature-first Clean Architecture — every feature is a self-contained module with the same five layers.
| Layer | Responsibility | Why it’s there |
|---|---|---|
domain | Entities and business rules | Zero dependencies — survives framework swaps |
application | Use cases and state notifiers | Orchestrates UI and data; holds the feature’s logic |
infrastructure | API services, repositories, DTOs | Network and persistence details, kept out of the UI |
presentation | Pages and widgets | Talks only to notifiers, never to DTOs |
shared | Riverpod providers | Dependency injection for the feature |
The boundaries are the architecture. Presentation never touches DTOs. Infrastructure never knows about the UI. Each layer can change without the others noticing.
State and data flow#
- State: Riverpod
NotifierProviderdriving Freezed state unions. Each feature’s state is a sealed set —initial → loadInProgress → loadSuccess → loadFailure. Exhaustive and typo-proof. - Repositories: interface defined in
domain, implemented ininfrastructure. The UI depends on the interface, not the network. - DTO → Entity: the API returns data-transfer objects; repositories convert them to domain entities. The UI never sees raw API shapes.
- Errors: the
dartzpackage’sEither<Failure, Success>. Failures are values the type system tracks, not exceptions scattered through call stacks.
Navigation, networking, and the rest#
- Navigation:
auto_routegenerates type-safe routes, with deep linking and route observers wired for analytics. A bad route is a compile error, not a runtime crash. - Networking: three
dioclients — one authenticated for the main API, one for the content-management system, one authless for general calls. Auth interceptors live in one place, not copy-pasted per request. - Code generation: Freezed, JSON serialization, and auto_route all run through
build_runner. Boilerplate is generated, not hand-written — less drift, fewer bugs. - Shared platform: auth, feed, and core utilities live in reusable shared packages — extracted once and reused across apps. This is a platform, not a one-off.
- Real-time and release hardening: Pusher channels carry live updates; Firebase handles messaging, analytics, and remote config; release builds are obfuscated with split debug symbols for size and symbolication.
Scale: 70+ Flutter Modules, Solo-Owned#
The architecture’s real test is volume. The app has over seventy feature modules, grouped by domain:
- Academic: advising, attendance, class schedule, announcements, LMS integration, certification, externship, financial ledger.
- Campus services: campus map, safety, parking, facilities and maintenance requests, staff directory.
- Student services: payments, profile, digital ID and QR code, document upload, transcript requests, veterans’ benefits.
- Communication: live feed, feedback, IT support, messaging, absence reporting.
I maintained all of it solo for six years — one architect holding the whole graph coherent across three stores. The architecture is the only reason that’s possible: every module reads the same way, so context never resets when you move between features. A team joined over the last year, and onboarding was straightforward for exactly that reason — a mid-level engineer can pick up any module and keep it shipping, because the same pattern repeats everywhere.
The Apple Review Battle#
Students sign in with Google. There is no “Sign in with Apple.”
That’s a deliberate choice with a cost. Apple’s App Store requires apps that offer third-party login to also offer Sign in with Apple, and review pushed back on the submission. The argument that won was institutional: this is an education-only app whose login is the college’s own identity system, not consumer social auth. I worked that justification through review and appeal until it was approved.
The app ships today, Google-only, exactly as designed — and the missing Sign in with Apple button is right there on the public App Store listing for anyone to see.
The transferable lesson: knowing the App Store guidelines and persisting through appeal is its own engineering skill. A rejection isn’t a no; it’s the start of a conversation about which rule actually applies.
A note on accuracy: I’m describing the review outcome qualitatively. I’m not citing a specific guideline subsection, because the exact framing that cleared review is the kind of detail worth confirming before you generalize it.
Long-Term Flutter Maintenance, Zero Incidents#
Zero major production incidents across the maintenance window. No data loss, no auth outage, no crash loop that took the app down for a meaningful slice of users. Not “few.” Zero.
That record is four habits compounded:
- Manual verification on the paths that hurt a student. The load-bearing flows — auth, payments, the API contract, attendance — get eyes on them on a real device before every promotion. Not a sprawling automated suite; deliberate, scoped manual passes, plus a CI-gated staged rollout that catches what a person could miss. I don’t chase 100% coverage as a vanity number — I cover what would hurt a student.
- Staged testing. Every release reaches real testers first — Android APKs and iOS TestFlight — before any store. That started as a manual hand-out; it’s now automated, so a git tag ships a signed build.
- Conservative dependency upgrades. Read the changelogs and issue trackers. Wait a beat on major bumps to see who breaks first.
- Monitoring. The stores’ own vitals dashboards — Play Console and App Store Connect crash data — plus an uptime monitor on the API. You see a wobble before the support ticket arrives.
The framework migrations ridden along the way — null safety, Material 3, Impeller, Dart 3 — are written up in detail in maintaining a Flutter app long-term.
Mobile CI/CD Pipeline Across Three Stores#
A merged pull request builds, tests, signs, and ships to all three stores without a human touching a console.
- Android (Play + Amazon): Drone CI builds, signs with the production keystore, uploads to the internal track, and promotes on a release commit.
- iOS: Xcode Cloud — Apple’s own CI keeps code signing and the App Store API inside Apple’s trust boundary. That removes a whole class of certificate-handling pain.
- Bitbucket Pipelines runs an AI code-review step on every change.
- Test builds by git tag: anyone — a junior dev, QA, a product owner — pushes a tag like
test-2026-07-08-featureand the pipeline emits a signed test build. The git tag is the release button.
The first review pass is now effectively instant. The AI flags correctness issues — null handling, missing error paths — plus architectural drift, like a feature reaching into another feature’s internals, and dependency and security smells. What it doesn’t catch is judgment: an API whose error contract quietly changed, a widget quirk that only surfaces on a real device. That’s still the human pass, and it’s why I sign off on every merge. The AI moves the review queue; it doesn’t own the release call.
The full stack is written up in the complete mobile CI/CD pipeline and how AI-assisted delivery works.
Outcome#
Seven-plus years. Three stores. Seventy-plus modules. Reviews that land in minutes instead of days. Releases that ship without a human at a console. Zero major production incidents — all on one Clean Architecture chosen in year two.
The NCC App is the project I point to when someone asks what “senior” actually means in mobile engineering. It’s not the flashiest build. It’s the one that stays up.
Lessons#
- Start messy, refactor to last by year two. Ship fast, then earn the right to a long horizon by rebuilding the seams before the codebase fights back.
- Treat the framework as a dependency, not a foundation. Keep domain logic away from Flutter so a Material 3 migration is a UI task, not a rewrite.
- Fight the platform fight worth fighting. App Store policy is navigable with the right argument and persistence.
- Keep the human in the release decision. AI moves the queue; judgment prevents the incident.
Frequently Asked Questions#
Is Clean Architecture worth it for a Flutter app? Yes, for any app with a multi-year or multi-developer horizon. The upfront cost pays back the first time a framework migration is a UI task instead of a rewrite. This app’s architecture has held for more than five years.
How long can a Flutter app be maintained? Seven-plus years here, and counting. The deciding factor isn’t Flutter — it’s keeping a seam between your domain logic and the framework so the codebase survives version churn.
How do you ship a Flutter app to three stores? Drone CI builds and uploads Android (Google Play and Amazon), Xcode Cloud handles iOS signing inside Apple’s trust boundary, and a git tag triggers test builds. Fully automated.
What state management does this app use?
Riverpod NotifierProvider with Freezed state unions, and repositories returning Either<Failure, Success> for typed errors.
Building or maintaining a Flutter app at the multi-year horizon? Get in touch — or read the guide to hiring a Flutter developer in 2026.

