Mobile Engineering

Your users keep an app for years. Most are built to survive one release.

The hard part of mobile is not the first build. It is the twentieth: staying fast, crash-free, and easy to change as features pile up and the network stays unreliable. That is what we engineer for.

3
clean layers keep features cheap to change
0
network calls the UI has to wait on, by design
1%
of users see a release before 100 percent do
live
crash-free rate tracked on every release

Why mobile apps fail

Six places mobile apps quietly rot

Most apps do not fail at launch. They fail slowly, over the year after, as changes get harder and quality slips. Here is where it usually starts.

Unmaintainable by version 20

UI, networking, and business rules end up in the same files. Every new feature touches everything, regressions multiply, and a two-day change becomes a two-week one.

The network is treated as reliable

Apps built on a fast office wifi assumption fall apart on a train, in a lift, or on a rural connection. Users see spinners, lost input, and duplicate submissions.

Releases are a leap of faith

No feature flags, no staged rollout. A bad build reaches every user at once, and the only fix is another full store review cycle that takes days.

Jank, cold starts, and battery drain

Heavy work runs on the UI thread, images load unoptimized, and lists rebuild on every frame. The app technically works and still feels cheap.

One codebase, two diverging apps

Cross-platform starts as a saving and becomes a tax when platform differences leak everywhere instead of living behind clean adapters.

Crashes you learn about from reviews

Without crash reporting, performance traces, and release health, the first sign of a regression is a one-star review and a drop in retention.

How we think

The engineering habits behind a long-lived app

These decisions are what separate an app you can grow for years from one that gets rebuilt in eighteen months.

Layer the app so features stay cheap to change

Presentation, domain, and data are separated behind clear boundaries. Business logic is pure and testable, and the UI is a thin layer over well-defined state.

Design for the network you actually have

Offline and flaky connectivity are the default assumption. Local state is the source of truth for the UI, and sync reconciles with the server in the background.

Every release is reversible

Feature flags and staged rollouts mean a new feature reaches 1 percent before 100 percent, and a problem is switched off in seconds without a new store submission.

Performance is a budget, measured on real devices

Cold start, frame times, and memory are tracked against targets on mid-range hardware, not just the latest flagship phone in the office.

Test at the boundary that catches real bugs

Fast unit tests cover domain logic, widget tests cover UI behavior, and a small set of integration tests protects the critical journeys. Coverage lives where it pays off.

Ship with a health dashboard from day one

Crash-free rate, ANR rate, and startup time are visible per release. A regression shows up on a chart before it shows up in your ratings.

Our engineering pipeline

From discovery to a monitored release

Every stage exists to prevent a specific, expensive failure later.

  1. 01

    Discovery & Product Mapping

    Understand the users, the devices they carry, the networks they use, and the one or two journeys that define the product.

    Prevents
    Prevents building for ideal conditions that real users never experience.
    You get
    Scope is grounded in how the app will actually be used.
  2. 02

    Architecture & Layering

    Define the presentation, domain, and data layers, the state approach, and the boundaries platform code lives behind.

    Prevents
    Prevents the tangle that makes an app unmaintainable a year in.
    You get
    New features slot into a predictable structure instead of fighting it.
  3. 03

    Design System & UX

    Build a component library and interaction patterns so screens stay consistent and quick to assemble.

    Prevents
    Prevents visual drift and one-off widgets nobody can safely reuse.
    You get
    The product looks coherent and ships faster as it grows.
  4. 04

    Data & Offline Layer

    Implement local persistence, a sync engine, and conflict handling so the app is fully usable without a connection.

    Prevents
    Prevents lost input, duplicate writes, and dead screens on poor networks.
    You get
    The app feels instant and keeps working where competitors stall.
  5. 05

    Feature Development

    Build features behind flags, with domain logic covered by tests and platform integrations wrapped in adapters.

    Prevents
    Prevents half-finished work blocking a release and platform quirks leaking into logic.
    You get
    Work merges continuously and stays shippable at all times.
  6. 06

    Release Engineering

    Automate builds and store submission, and roll out in stages with the ability to halt or roll back instantly.

    Prevents
    Prevents a bad build reaching every user with no quick way back.
    You get
    Releases become routine and low-drama instead of a monthly gamble.
  7. 07

    Monitoring & Iteration

    Watch crash-free rate, performance, and adoption per release, and feed what you learn into the next cycle.

    Prevents
    Prevents silent regressions and guesswork about what to build next.
    You get
    Quality trends up and the roadmap follows real usage.

Field notes

Four things we get right that most teams skip

The actual reasoning we bring to a mobile engagement. Expand any section for the full breakdown.

01 · Maintainability

Why most mobile apps become unmaintainable after 12 to 18 months

Almost every struggling mobile codebase started clean. The rot sets in gradually, one shortcut at a time, until a change that should take an afternoon takes a week and nobody is quite sure what it might break.

The common root cause is the absence of layers. Networking, business rules, and UI end up interleaved in the same screens, so every feature reaches into everything, tests are impossible to write, and the app becomes a web of hidden dependencies that only its original authors can navigate.

Expand the full engineering breakdown

How the decay happens

State management is usually the first thing to slip. A screen holds its own data, then another screen needs the same data, so it gets copied or lifted awkwardly. Soon the same information lives in three places that disagree. Meanwhile API models are used directly in the UI, so a change to a backend response ripples straight into widgets, and there is no safe place to put a fix.

How we prevent it

We separate the app into three layers. The presentation layer renders state and captures intent. The domain layer holds business rules as pure logic with no framework imports, which makes it fast to test and stable across UI changes. The data layer owns repositories, local storage, and mapping between API models and domain entities, so external changes stop at the boundary. The rule is simple and enforced: dependencies point inward toward the domain.

Trade-offs we make explicit

Layering adds a little structure up front. For a two-week throwaway prototype, that overhead is not worth it. For anything you intend to run for years, it pays for itself the first time a backend contract changes or you swap a state library, because the blast radius is contained to one layer instead of the whole app.

The takeaway

Maintainability is not a feature you add later. It is the direct result of boundaries you set on day one. A change in year two should cost about what it cost in month two, and clean layers are how you get there. We wrote about this in depth for Flutter specifically, linked below.

02 · Reliability

Designing offline-first applications for unreliable networks

Most apps are built and tested on excellent connectivity, then shipped to users on trains, in basements, and on patchy mobile data. The result is a product that spends its life showing spinners and error toasts in exactly the moments people need it most.

Offline-first flips the default. The app assumes the network is unavailable and treats it as a bonus when it is present, which produces something that feels faster even on a good connection.

Expand the full engineering breakdown

The local store is the source of truth

The UI reads from a local database, never directly from the network. Screens render instantly from cached data, and a background sync process reconciles with the server when a connection is available. This single decision removes most loading states and makes the app usable end to end without a signal.

Writes are queued, not fired and forgotten

A user action updates local state immediately and enqueues a durable operation to sync. If the connection drops mid-action, the operation waits and completes later, with idempotency keys so a retry never creates a duplicate. The person who submitted a form in a lift sees it succeed, because the app was never depending on that exact moment of connectivity.

Conflicts are a design decision

When the same record changes on device and server, someone has to decide who wins. We choose a strategy per data type rather than pretending conflicts will not happen: last-write-wins for low-stakes fields, server-authoritative for shared records, and field-level merges where it matters. The point is that the behavior is deliberate and predictable, not accidental.

The takeaway

Offline-first is not a feature for a subset of users. It is an architecture that makes the app faster and more reliable for everyone, and it is far cheaper to design in from the start than to retrofit once the UI already assumes a live connection.

03 · Scale

Scaling push notifications to millions of devices

A push notification looks like a single API call in the docs. At scale it becomes a distributed systems problem: fan-out, rate limits, token hygiene, targeting, and measurement, all of which quietly break the naive approach the moment your audience grows.

The teams that get burned are the ones that loop over a user table and call the provider inline. That works for a thousand users and falls over at a million.

Expand the full engineering breakdown

Fan-out through a queue

A campaign becomes a job, not a loop. Workers pull from a queue, batch sends to the provider, and respect its rate limits with backpressure, so a burst to millions of devices flows at a controlled, sustainable rate instead of overwhelming the provider or your own servers. Throughput scales by adding workers, not by praying.

Token hygiene is non-negotiable

Device tokens expire, rotate, and go stale as users uninstall. Without cleanup, you waste spend pushing to dead endpoints and skew your delivery metrics. We process provider feedback to prune invalid tokens continuously, so the audience stays real and sends stay efficient.

Targeting and measurement

Blasting everyone is how you train users to disable notifications. We segment by behavior and preference, respect quiet hours and frequency caps, and track delivery, open, and downstream action rates so every campaign is measurable. Retries are bounded so a provider outage delays a batch rather than duplicating it.

The takeaway

Push at scale is an infrastructure problem wearing an API's clothing. Queue it, batch it, keep tokens clean, target with care, and measure everything. Done well, it is a retention engine. Done naively, it is an outage and an uninstall wave.

04 · Release safety

Managing releases safely with feature flags and staged rollouts

Mobile has a property web does not: you cannot instantly fix a bad release. A broken build sits in front of users until a new binary clears store review, which can take days. That single fact should shape how every mobile team ships, and for most it does not.

Feature flags and staged rollouts turn an irreversible event into a controlled, reversible one.

Expand the full engineering breakdown

Decouple deploy from release

Shipping a binary and turning a feature on become two separate events. New code ships dark, behind a flag that is off. When you are ready, you enable it server-side for a small slice of users. The binary in the store never changes, so enabling and disabling a feature is instant and needs no review cycle.

Roll out in stages, watch the health signals

A feature reaches 1 percent, then 10, then 50, then everyone, and at each step we watch crash-free rate, ANR rate, and key funnel metrics for that cohort. A regression shows up in the small group first, where it affects few users and is easy to attribute, rather than in a support queue after a full launch.

A kill switch for the moments that matter

Every risky feature ships with the ability to switch it off remotely. When something goes wrong at 2am, the on-call response is a config toggle, not an emergency build and a plea to the review team. Flags also need lifecycle discipline, so we retire them once a feature is stable to avoid a different kind of mess.

The takeaway

Because mobile releases are slow to reverse, safety has to be built into how you roll out, not bolted on after an incident. Flags and staged rollout are the difference between a quiet Tuesday and a five-star rating in free fall.

Deeper read: Designing a production-grade Flutter app: clean layers, sharp boundaries.

Reference architecture

What a durable mobile app looks like inside

Not "app calls API". A resilient mobile product has a local store, a sync engine, and clear boundaries, each there for a reason.

UI (Flutter / native) · pure state
Domain layer · business rules
Local store · SQLite / Drift
Sync engine · queue + conflict handling
API gateway · auth, rate limit
Backend services
Push service · queued fan-out
Analytics + crash + release health

UI reads pure state

Widgets render immutable state and emit intent. No networking or storage logic lives in the view, so screens stay simple and testable.

Domain holds the rules

Business logic is pure Dart with no framework imports. It is the most valuable and most stable part of the app, and the easiest to test.

Local store as source of truth

The UI reads from a local database so it renders instantly and works offline. The network updates the store, not the screen directly.

Sync engine reconciles

A background process queues writes, retries with idempotency, and resolves conflicts by a strategy chosen per data type.

Gateway and backend

Auth, rate limiting, and API shaping sit server-side. A backend-for-frontend keeps the app thin and payloads screen-shaped.

Health from day one

Crash-free rate, ANR, startup time, and adoption are tracked per release, so regressions surface on a chart, not in reviews.

Scaling mobile

What we engineer for growth

  • Background sync & queues. Data and writes move off the UI thread and off the critical path, so the app stays responsive under load and poor coverage.
  • Push fan-out. Campaigns are queued, batched, and rate-limited, so reaching millions of devices is a throughput knob, not an outage.
  • Image & asset pipeline. Right-sized, cached, and lazily loaded media keeps scrolling smooth and memory in check on mid-range devices.
  • Pagination & caching. Lists page and cache instead of loading everything, so screens stay fast as data grows.
  • Feature flags & staged rollout. Rollouts are gradual and reversible, which keeps a bad change contained to a small cohort.
  • Performance budgets. Cold start, frame time, and memory are tracked against targets on real hardware, not just flagships.

Securing mobile

Where we protect your users

  • Secure on-device storage. Tokens and sensitive data live in the Keychain and Keystore, never in plain preferences or logs.
  • Certificate pinning. The app validates it is talking to your servers, which blocks a whole class of interception attacks on untrusted networks.
  • Robust auth & token refresh. Short-lived tokens with silent refresh and secure session handling, plus optional biometric gates for sensitive actions.
  • Least-data by default. We collect and cache only what the feature needs, and redact personal data from logs and crash reports.
  • Tamper & reverse-engineering resistance. Release builds are obfuscated and hardened to raise the cost of tampering and secret extraction.
  • OWASP MASVS baseline. Mobile security is measured against the recognized standard, not improvised.

Reference: OWASP Mobile Application Security

Honest trade-offs

What we would choose, and when we would not

There is no universally right stack, only the right call for your product and constraints.

We reach for Over When
Flutter Fully native (two codebases) You want one team, one codebase, and near-native performance across iOS and Android, with heavy custom UI. Our default for most products.
Native (Kotlin / Swift) Cross-platform The product lives or dies on deep platform integration, the latest OS features, or extreme performance in a narrow domain.
Offline-first local store Online-only Users work on the move or in poor coverage, or the app must feel instant. Most consumer and field apps benefit.
Feature flags + staged rollout Ship to everyone at once Always, once you have real users. The cost is small and the safety is large.
A backend-for-frontend The app calling many services The client would otherwise orchestrate several APIs. A BFF keeps the app thin and the payloads shaped for the screen.

Why teams choose Averon

The reasons teams keep building with us

Change stays cheap for years

Clean layers mean a feature in year two costs about what it did in month two, so your roadmap keeps moving instead of slowing to a crawl.

Reliability your users feel

Offline-first data and disciplined performance work produce an app that stays fast and usable in the real conditions people live in.

Releases without the fear

Feature flags and staged rollout turn shipping into a routine, reversible event, which protects both your ratings and your team's weekends.

Quality you can see

Crash-free rate and performance are on a dashboard per release, so problems are caught early and fixed deliberately.

One team, both platforms

A single well-architected codebase serves iOS and Android at near-native quality, which lowers cost without lowering the bar.

We build the whole product

App, backend, sync, and infrastructure are engineered together as one system we can own end to end.

Engineering FAQ

The questions serious teams ask

Yes. Flutter runs demanding production apps with complex UI, offline data, payments, and real-time features. The limits people hit are almost always architecture and process, not the framework. With clean layering, a solid offline strategy, and disciplined release engineering, a Flutter app competes with native on quality while costing far less to maintain across two platforms.

We design offline-first. The local database is the source of truth for the UI, so screens render instantly from cached data while a background sync engine reconciles with the server. Writes are queued and retried with conflict handling, so a submission made in a tunnel completes when the connection returns instead of failing or duplicating. The user rarely sees a spinner because the app is not waiting on the network to be usable.

We separate the app into presentation, domain, and data layers with clear boundaries, keep business logic pure and covered by fast tests, and confine platform-specific code behind adapters. New features extend a predictable structure rather than adding to a pile. The result is that a change in year two costs about what it cost in month two, which is the real measure of a healthy codebase.

Releases go out behind feature flags and staged rollout. A new feature is enabled for a small percentage of users first while we watch crash-free rate and performance, then widened as it proves stable. If something regresses, the flag switches it off in seconds, with no new app store review required. This turns releases from a monthly risk into a routine, reversible event.

Often, yes. We start by mapping the current architecture and the riskiest areas, then improve incrementally rather than attempting a risky rewrite. That usually means introducing clean boundaries at the edges, adding tests around critical logic, and modernizing one area at a time so the app keeps shipping throughout. Where a rewrite genuinely is the right call, we say so and plan it to run alongside the live app.

Sending to millions of devices is a fan-out and reliability problem, not a single API call. We queue sends, batch them, respect provider rate limits, handle token rotation and invalidation, and segment audiences so the right users get the right message. Delivery and open rates are tracked so campaigns are measurable, and retries are bounded so a provider hiccup never becomes a flood.

Building a mobile product that has to last?

If you are planning software that needs to survive years of growth, not just launch day, we would be glad to discuss the architecture before writing a single line of code.

Talk architecture with our engineers