<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Zain Naseer</title>
    <description>The latest articles on DEV Community by Muhammad Zain Naseer (@muhammadzainnaseer).</description>
    <link>https://dev.to/muhammadzainnaseer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4002004%2F5c188ba2-204e-4318-a529-aee441447174.png</url>
      <title>DEV Community: Muhammad Zain Naseer</title>
      <link>https://dev.to/muhammadzainnaseer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammadzainnaseer"/>
    <language>en</language>
    <item>
      <title>REST vs. GraphQL: A Decision Framework, Not a Holy War</title>
      <dc:creator>Muhammad Zain Naseer</dc:creator>
      <pubDate>Thu, 25 Jun 2026 09:24:31 +0000</pubDate>
      <link>https://dev.to/muhammadzainnaseer/rest-vs-graphql-a-decision-framework-not-a-holy-war-oh9</link>
      <guid>https://dev.to/muhammadzainnaseer/rest-vs-graphql-a-decision-framework-not-a-holy-war-oh9</guid>
      <description>&lt;p&gt;Ask which API style you should use and you'll get answers with the emotional intensity of a religious debate. GraphQL evangelists will tell you REST is a legacy relic. REST loyalists will tell you GraphQL is complexity cosplay. Both camps are arguing about the wrong thing — because the right choice almost never depends on which technology is "better." It depends on the shape of your data, the shape of your clients, and the shape of your team.&lt;/p&gt;

&lt;p&gt;Here's a framework for deciding based on your actual situation instead of the loudest blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one is actually good at
&lt;/h2&gt;

&lt;p&gt;Strip away the tribalism and the core difference is simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; gives you a set of URLs, each returning a fixed, server-defined shape of data. &lt;code&gt;/users/42&lt;/code&gt; returns a user. &lt;code&gt;/users/42/orders&lt;/code&gt; returns their orders. The server decides what each endpoint contains; the client takes what it's given. This is predictable, cacheable by default, and trivially understood by anyone who's used the web.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL&lt;/strong&gt; gives you a single endpoint and a query language. The client describes exactly the data it wants — these fields from the user, those fields from each of their orders, nested however it likes — and gets back precisely that shape in one round trip. The power moves to the client.&lt;/p&gt;

&lt;p&gt;That single distinction — &lt;em&gt;who decides the response shape&lt;/em&gt; — drives almost everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problems GraphQL was built to solve
&lt;/h2&gt;

&lt;p&gt;Two specific pains push teams toward GraphQL, and if you don't have them, you don't need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-fetching and under-fetching.&lt;/strong&gt; With REST, an endpoint returns a fixed payload. A mobile screen that needs only a user's name and avatar still downloads their full profile — bio, settings, timestamps, the works (over-fetching). Meanwhile, a screen that needs a user &lt;em&gt;plus&lt;/em&gt; their last three orders &lt;em&gt;plus&lt;/em&gt; each order's status has to hit three endpoints and stitch the results together client-side (under-fetching). GraphQL collapses both problems: ask for exactly the fields you need, across exactly the relationships you need, in one query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many diverse clients evolving fast.&lt;/strong&gt; If you have a web app, an iOS app, an Android app, and a partner integration — each needing a different slice of the same data, each shipping on its own schedule — REST tends to sprout either bloated do-everything endpoints or a proliferation of bespoke ones. GraphQL lets each client self-serve from a single schema without the backend cutting a new endpoint every sprint.&lt;/p&gt;

&lt;p&gt;If those two paragraphs describe your life, GraphQL is earning its complexity. If they don't, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problems GraphQL quietly introduces
&lt;/h2&gt;

&lt;p&gt;GraphQL isn't free. It moves complexity rather than removing it, and the new complexity lands in places that are easy to underestimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching gets harder.&lt;/strong&gt; REST rides on a decade of HTTP caching infrastructure — CDNs, browser caches, &lt;code&gt;ETag&lt;/code&gt;s — all keyed on URLs. GraphQL's single POST endpoint defeats most of that out of the box. You get caching back, but you build it yourself at the application or client layer, and it's real work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance footguns multiply.&lt;/strong&gt; The flexibility that lets a client ask for deeply nested data also lets it ask for &lt;em&gt;expensive&lt;/em&gt; deeply nested data. The infamous N+1 query problem — one query fanning out into hundreds of database calls — is a constant hazard, and you'll need tools like DataLoader-style batching and query-cost analysis to keep it contained. A naive GraphQL server is a denial-of-service vulnerability waiting for a curious client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability and security need new habits.&lt;/strong&gt; Every REST endpoint is a natural unit for rate limiting, monitoring, and access control. With one GraphQL endpoint, you have to reconstruct all of that at the field and query-cost level. It's doable and well-trodden, but it's a different mental model your whole team has to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision framework
&lt;/h2&gt;

&lt;p&gt;Forget which is trendier. Walk these questions in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. How many clients consume this API, and how different are their data needs?&lt;/strong&gt;&lt;br&gt;
One client, or several with near-identical needs → REST. Multiple clients with genuinely divergent, fast-changing needs → GraphQL starts paying off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How relational and nested is your data?&lt;/strong&gt;&lt;br&gt;
Mostly flat resources fetched independently → REST fits naturally. Deeply connected graphs where clients routinely need "this, plus its related that, plus their related other thing" → that's literally what GraphQL is named for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. How much does HTTP caching matter to you?&lt;/strong&gt;&lt;br&gt;
Public, cacheable, read-heavy content (think a content site or product catalog) → REST's free CDN caching is a serious advantage. Highly personalized, per-user data that wasn't going to cache well anyway → you lose less by going GraphQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What does your team already know?&lt;/strong&gt;&lt;br&gt;
This one is underrated. A team fluent in REST that adopts GraphQL for a CRUD app will spend its first quarter relearning caching, error handling, and performance tuning it already had for free. Technology you can operate confidently beats technology that's theoretically optimal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Do you actually have the GraphQL pains today?&lt;/strong&gt;&lt;br&gt;
Not "might we someday." Today. If you're over-fetching badly or drowning in client-specific endpoints right now, that's a real signal. If you're choosing GraphQL for a problem you're imagining, you're buying complexity on credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The answer nobody wants to hear
&lt;/h2&gt;

&lt;p&gt;For a large share of products — a single app talking to a backend, mostly straightforward resources, a small team — &lt;strong&gt;REST is the right call&lt;/strong&gt;, and it's not close. It's simpler to build, simpler to cache, simpler to secure, and simpler to hire for. "Boring" is a feature when it's load-bearing.&lt;/p&gt;

&lt;p&gt;GraphQL is a genuinely excellent answer to a specific set of problems: many clients, deeply relational data, and rapid independent evolution. When you have those problems, it's transformative. When you don't, it's a sophisticated solution looking for a question.&lt;/p&gt;

&lt;p&gt;And you're not locked in. Plenty of mature systems run REST for their cacheable public surface and GraphQL for their complex internal client needs. The two aren't enemies — they're tools with different sweet spots, and the senior move is matching the tool to the shape of the work rather than to the shape of the argument.&lt;/p&gt;

&lt;p&gt;Pick the one your situation is asking for. Let everyone else fight about it online.&lt;/p&gt;

</description>
      <category>api</category>
      <category>graphql</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to Put an LLM in Your Product Without Wrecking Your Costs or Your Latency</title>
      <dc:creator>Muhammad Zain Naseer</dc:creator>
      <pubDate>Thu, 25 Jun 2026 09:21:40 +0000</pubDate>
      <link>https://dev.to/muhammadzainnaseer/how-to-put-an-llm-in-your-product-without-wrecking-your-costs-or-your-latency-89a</link>
      <guid>https://dev.to/muhammadzainnaseer/how-to-put-an-llm-in-your-product-without-wrecking-your-costs-or-your-latency-89a</guid>
      <description>&lt;p&gt;Adding an AI feature looks deceptively easy. You sign up for an API key, paste in a prompt, and within an hour you've got a working demo that makes the whole team lean over your shoulder. Then you ship it, traffic arrives, and two things happen at once: your latency graph develops a long, ugly tail, and your monthly bill arrives with a number that makes finance schedule a meeting.&lt;/p&gt;

&lt;p&gt;The gap between "impressive demo" and "production feature" is almost entirely about cost and latency engineering. The model is the easy part. Here's how to cross that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, understand what you're actually paying for
&lt;/h2&gt;

&lt;p&gt;Most LLM APIs bill by &lt;strong&gt;tokens&lt;/strong&gt; — roughly ¾ of a word each — and they bill &lt;em&gt;both&lt;/em&gt; directions: the tokens you send (input) and the tokens the model generates (output). Output tokens are usually several times more expensive than input tokens, which has a non-obvious consequence: a verbose prompt is cheaper than a verbose answer.&lt;/p&gt;

&lt;p&gt;This reframes optimization. People obsess over trimming their prompts while letting the model ramble for 800 tokens when 80 would do. If you want to cut cost, the highest-leverage move is almost always &lt;strong&gt;constraining the output&lt;/strong&gt;: ask for JSON, ask for a single sentence, set a &lt;code&gt;max_tokens&lt;/code&gt; ceiling, and tell the model explicitly to be terse.&lt;/p&gt;

&lt;p&gt;Latency follows the same logic. Generation is sequential — the model produces one token at a time — so output length is the single biggest driver of how long a request takes. A 50-token answer is fast almost regardless of model. A 2,000-token answer is slow even on the fastest infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 1: Don't call the model when you don't have to
&lt;/h2&gt;

&lt;p&gt;The cheapest, fastest LLM call is the one you never make. Two techniques eliminate a startling share of traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching identical and near-identical requests.&lt;/strong&gt; Many real-world prompts repeat — the same FAQ-style question, the same document summarized twice, the same classification of similar inputs. A cache keyed on the normalized prompt turns a repeat request into a sub-millisecond lookup. For exact repeats, a simple key-value cache works. For &lt;em&gt;similar&lt;/em&gt; requests, a semantic cache — where you embed the query and return a cached answer if a previous query is close enough in vector space — can absorb far more traffic, at the cost of some tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing to the right tier.&lt;/strong&gt; You do not need your most capable model for every task. Classifying a support ticket into one of five buckets is a job for a small, cheap, fast model. Drafting a nuanced customer email is worth the premium one. A simple router — even a keyword or length heuristic before anything fancy — that sends easy work to a cheap model and hard work to an expensive one can cut spend dramatically without anyone noticing a quality drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 2: Make latency feel lower than it is
&lt;/h2&gt;

&lt;p&gt;Sometimes you genuinely need a long, high-quality response, and it's genuinely going to take a few seconds. You can't always make it faster — but you can make it &lt;em&gt;feel&lt;/em&gt; fast, which is often what actually matters to the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stream the response.&lt;/strong&gt; Instead of waiting for the full answer and dumping it at once, stream tokens as they're generated. The user starts reading after a few hundred milliseconds, and the perceived wait collapses even though total generation time is unchanged. This is the single highest-impact UX change for any chat-style feature, and most SDKs support it with a one-line change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show honest progress for non-streamed work.&lt;/strong&gt; If you're doing something multi-step — retrieve, then reason, then format — tell the user what's happening ("Searching your documents…", "Drafting an answer…"). A visible, truthful status beats a spinner that gives no information about whether anything is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 3: Control the worst case, not just the average
&lt;/h2&gt;

&lt;p&gt;Your average latency is a comforting lie. LLM endpoints have heavy tails: most requests are fine, but a meaningful slice take 3–5× longer, and a few time out entirely. If your product blocks on those, a small fraction of slow requests can dominate the experience.&lt;/p&gt;

&lt;p&gt;Defend against the tail explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set aggressive timeouts&lt;/strong&gt; and decide in advance what happens when you hit one — a cached fallback, a smaller model, a graceful "try again" — rather than letting the request hang.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a retry with backoff&lt;/strong&gt; for transient failures, but cap it. Infinite retries against an overloaded provider just make the outage worse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a circuit breaker&lt;/strong&gt; for sustained failures. If the provider is clearly down, fail fast to your fallback instead of sending every user into a 30-second wait.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't AI-specific patterns — they're the same resilience engineering you'd apply to any external dependency. The mistake is treating the LLM as magic instead of as what it is: a slow, occasionally flaky network call to someone else's servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever 4: Measure the things that actually move
&lt;/h2&gt;

&lt;p&gt;You can't optimize what you don't track. From day one, log three numbers per request: &lt;strong&gt;input tokens, output tokens, and end-to-end latency.&lt;/strong&gt; Tag them by feature and by model. Within a week you'll have a cost-and-latency breakdown by feature, and it will almost certainly surprise you — there's usually one endpoint quietly responsible for most of the bill, and it's rarely the one you'd guess.&lt;/p&gt;

&lt;p&gt;A useful derived metric is &lt;strong&gt;cost per successful user outcome&lt;/strong&gt;, not cost per API call. A feature that calls the model twice but actually solves the user's problem is cheaper, in every way that matters, than one that calls it once and gets ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mindset shift
&lt;/h2&gt;

&lt;p&gt;The teams that ship AI features sustainably stop thinking of the model as the product and start thinking of it as an expensive, high-variance dependency they're responsible for managing. The prompt gets you the demo. Caching, routing, streaming, and tail control get you a feature you can afford to keep running.&lt;/p&gt;

&lt;p&gt;None of it is exotic. It's the same discipline that makes any external service production-ready — applied to a service that happens to charge by the word and answer at the speed of thought, one token at a time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Structure a Scalable Flutter App Before It Structures You</title>
      <dc:creator>Muhammad Zain Naseer</dc:creator>
      <pubDate>Thu, 25 Jun 2026 09:18:35 +0000</pubDate>
      <link>https://dev.to/muhammadzainnaseer/how-to-structure-a-scalable-flutter-app-before-it-structures-you-3i1d</link>
      <guid>https://dev.to/muhammadzainnaseer/how-to-structure-a-scalable-flutter-app-before-it-structures-you-3i1d</guid>
      <description>&lt;p&gt;Every Flutter app starts the same way: one &lt;code&gt;main.dart&lt;/code&gt;, a &lt;code&gt;StatelessWidget&lt;/code&gt;, and a feeling of total control. Then features arrive. A login screen needs the same user object the profile screen mutates. A network call that lived happily inside a widget now needs to be reused in three places. Six weeks in, you're scrolling through a 1,400-line widget file looking for the one &lt;code&gt;setState&lt;/code&gt; that's causing a rebuild storm, and "control" is the last word you'd use.&lt;/p&gt;

&lt;p&gt;The good news: scalable Flutter architecture isn't a framework you bolt on later. It's a small number of boundaries you draw early, and then refuse to cross. This guide walks through the boundaries that matter, why each one exists, and how to apply them without turning a simple app into an enterprise cathedral.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that prevents most pain
&lt;/h2&gt;

&lt;p&gt;If you take nothing else from this article, take this: &lt;strong&gt;your UI should never talk directly to your data sources.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A widget should not know whether the user's name came from a REST API, a local SQLite cache, or a hard-coded mock. The moment a widget knows that, you've welded your interface to your infrastructure. Swapping the API, adding a cache, or writing a test now means touching UI code — and UI code is the most volatile, most frequently rewritten code you own.&lt;/p&gt;

&lt;p&gt;The fix is to put layers between them. Three is usually enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Presentation&lt;/strong&gt; — widgets, and the state objects that feed them. This layer's only job is to render state and forward user intent ("the user tapped Save"). It holds no business rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Domain&lt;/strong&gt; — the logic that defines what your app actually &lt;em&gt;does&lt;/em&gt;. "A user can't check out with an empty cart." "A draft auto-saves after 5 seconds of inactivity." This layer is pure Dart: no Flutter imports, no HTTP, no database. That purity is the point — it's the part of your app that's trivially testable and rarely needs to change just because a button moved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Data&lt;/strong&gt; — repositories and the data sources behind them (API clients, local storage, platform channels). This layer knows about JSON, status codes, and cache invalidation so that nothing above it has to.&lt;/p&gt;

&lt;p&gt;Dependencies point in one direction only: Presentation → Domain → Data. The domain never imports the UI. The data layer never imports the domain's business logic. When you find yourself wanting to break that arrow, that's the signal you've put logic in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Say you're building a screen that shows a user's profile. Here's the shape, layer by layer.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;data layer&lt;/strong&gt; exposes a repository with an honest, source-agnostic interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepositoryImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;UserRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserApi&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserCache&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the caching strategy lives here and nowhere else. The screen above will never know it exists — and if you later switch from a read-through cache to stale-while-revalidate, the UI doesn't change at all.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;presentation layer&lt;/strong&gt; holds a state object that depends on the abstraction, not the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileNotifier&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ChangeNotifier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;ProfileNotifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AsyncValue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;notifyListeners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AsyncData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AsyncError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;notifyListeners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the widget just renders whatever state it's handed. Loading, error, and success are explicit states — not an afterthought you remember to handle after a crash report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a state management approach (without the holy war)
&lt;/h2&gt;

&lt;p&gt;Flutter's state management debate is exhausting because people argue about tools when the real question is about scope. Here's a pragmatic way to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral, single-widget state&lt;/strong&gt; (a checkbox, an expanded/collapsed panel): just use &lt;code&gt;setState&lt;/code&gt;. Reaching for a global solution here is over-engineering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared, app-level state&lt;/strong&gt; (the logged-in user, a shopping cart, feature flags): use a dependency-injected solution — Riverpod, Bloc, or &lt;code&gt;provider&lt;/code&gt; with &lt;code&gt;ChangeNotifier&lt;/code&gt;. Which one matters far less than using &lt;em&gt;one&lt;/em&gt; consistently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trap isn't picking the "wrong" library. It's mixing three of them in the same codebase because each feature was built by someone with a different favorite. Pick one, write it in your team's README, and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organize by feature, not by type
&lt;/h2&gt;

&lt;p&gt;A surprising amount of long-term maintainability comes from how you name folders. The instinct is to group by technical type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/
  widgets/
  models/
  services/
  screens/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works until you have forty screens, at which point every change to one feature means hopping across four directories. Group by feature instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/
  features/
    auth/
      data/
      domain/
      presentation/
    profile/
      data/
      domain/
      presentation/
  core/        // shared utilities, theming, networking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a feature is a vertical slice you can reason about — or delete — in one place. New developers can find things. And the layer boundaries from earlier show up right there in the folder names, which quietly enforces the discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where most teams actually go wrong
&lt;/h2&gt;

&lt;p&gt;The failure mode is almost never "we didn't have enough architecture." It's the opposite: a team reads a clean-architecture article, generates eleven files and four abstractions to display a single string, and concludes that architecture is bureaucracy.&lt;/p&gt;

&lt;p&gt;Scale your structure to your app. A weekend project doesn't need repositories. A two-screen utility might live happily with &lt;code&gt;setState&lt;/code&gt; and a couple of service classes. The three-layer model earns its keep when you have real business logic, multiple data sources, or a team larger than one — and you'll feel the pull toward it naturally as those conditions appear.&lt;/p&gt;

&lt;p&gt;The skill isn't applying every pattern. It's recognizing the moment a boundary will save you more than it costs, and drawing it then — one layer before the file gets to 1,400 lines, not one crisis after.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>architecture</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
