Stateless Classes Are Better: A Lesson from Flutter

Stop storing state in your classes. Localize it via providers, minimize your bug surface area, and write code that's actually testable.

Scan texture
Scan texture

Published On

Sat Dec 07 2025

0
/
0

Here's a pattern I keep seeing: developers build classes that hold state, mutate it across methods, and then wonder why their tests are a nightmare and their bugs multiply like rabbits. The problem isn't the language. It's that stateful classes turn your entire service into a minefield of side effects and race conditions.

Stateless classes are better. Not "sometimes better" or "better in certain situations." Just better. And if you need proof, look at Flutter. Or better yet, look at any distributed system that actually works.

The Problem with Stateful Classes

When you store state inside a class, every method that touches it becomes a potential source of bugs. You're not just mutating one variable—you're creating dependencies between every method that reads or writes that state. And now you have to worry about execution order, thread safety, and whether some random method halfway through your service just changed the value your current method relies on.

It gets worse when you introduce concurrency. Two requests hit the same instance? Race condition. One request fails and leaves dirty state? Every subsequent request is now broken. You're not building software at that point, you're playing bug whack-a-mole.

Stateful classes don't scale, they don't test well, and they don't fail gracefully. They just fail.

Localize State with Providers

Flutter figured this out early. Instead of storing state in widgets (classes), you pass it down via providers. State lives at the top of the tree, flows down through the UI, and gets updated in a controlled, predictable way. Widgets stay stateless. They render based on what they're given, not what they remember.

class UserProfile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final user = Provider.of<User>(context);
    return Text(user.name);
  }
}

The widget doesn't hold the user. It doesn't mutate the user. It just reads it and renders. If the user changes, the provider notifies downstream widgets. No hidden state. No side effects. No surprises.

This pattern works everywhere, not just in Flutter. You can apply it to backend services, request handlers, data pipelines—anywhere state becomes a problem.

Stateless Functions Build Up Your Request

In a request-response system, state should live in exactly one place: the request context. Not in your classes. Not in some global singleton. In the request.

Here's how it works. A request comes in. You parse headers, extract parameters, maybe pull user info from a session. That's your state. Now you pass it through a series of stateless functions that transform it, validate it, enrich it. Each function takes state as input and returns new state. No mutation. No side effects.

function validateUser(state: RequestState): RequestState {
  if (!state.user) throw new UnauthorizedError();
  return { ...state, validated: true };
}

function fetchUserData(state: RequestState): RequestState {
  const data = db.query('SELECT * FROM users WHERE id = ?', state.user.id);
  return { ...state, userData: data };
}

You chain these functions together. State flows through them. Each one does its job and hands the result to the next. When you're done, you execute your side effects—hit the database, fetch from S3, call an API—and build the response from that final state.

No class holding onto stale data. No method accidentally mutating something three steps earlier. Just pure functions and a single source of truth.

Easier to Test

Stateless classes are trivially easy to test because they have no memory. You don't need to set up state, tear down state, or worry about test isolation. You just call a function with inputs and check the output.

test('validates user correctly', () => {
  const input = { user: null };
  expect(() => validateUser(input)).toThrow(UnauthorizedError);
});

Done. No mocking a complex class hierarchy. No stubbing internal state. No worrying about whether your test left behind dirty data that breaks the next test. Just inputs and outputs.

Contrast that with stateful classes, where you need to:

  • Instantiate the class
  • Set up initial state
  • Call methods in the right order
  • Check internal state at each step
  • Reset everything between tests

It's exhausting. And fragile. One change to the class's internal state breaks half your tests.

Minimize Your Bug Surface Area

Every piece of mutable state is a potential bug. Every method that touches that state multiplies the risk. With stateful classes, your bug surface area grows exponentially as your codebase scales.

Stateless classes keep it linear. Each function operates on its inputs. If there's a bug, it's isolated to that function. You don't have to trace through a dozen method calls to figure out where state got corrupted. The function either works or it doesn't.

This is especially important in distributed systems. If state lives in your classes, you can't safely scale horizontally. Each instance has its own state. Requests can't be routed arbitrarily. You've just turned your stateless backend into a stateful mess.

But if your classes are stateless and state lives in the request? Scale as much as you want. Every instance is identical. Every request is independent. No coordination required.

Apply This to Everything

This isn't just for backends. It's for ETL pipelines. It's for data processing. It's for event-driven architectures. Anywhere you have state, you can apply this pattern.

Extract your state from the source—URL parameters, database row, S3 object, doesn't matter. Transform it through stateless functions. Load it into your destination. Classic ETL, but everywhere.

Even in stateful systems like web sockets or long-running workers, you can localize state to the session or job context and keep your classes stateless. State flows through your code, not sticking around in objects waiting to cause problems.

TL;DR

Stop putting state or network calls in your classes and ui components. It's not helping you. It's making your code harder to test, harder to debug, and harder to scale.

  • Localize state via providers or request contexts
  • Build your logic with stateless functions
  • Execute side effects (db, storage) at the edges
  • Keep your classes stateless and your tests simple

Stateful classes don't make your code flexible, they make it fragile. Stateless is better. Flutter proved it. Distributed systems prove it every day. Time to apply it everywhere else.

More writing

Recent posts.

Notes on engineering, design, and building products.

ai

CLIs Are for Robots, IDEs Are for Humans

A mental model for agentic coding workflows: where machines execute, where humans judge, and why keeping that distinction sharp makes everything work better.

ai

A Practical Pattern for Hydrating AI-Generated Object Templates

How I hydrate server-side LLM templates with client constants and API data using a queue-based pattern.

flutter

Stateless Classes Are Better: A Lesson from Flutter

Stop storing state in your classes. Localize it via providers, minimize your bug surface area, and write code that's actually testable.

data-visualization

Treat Your Chart Like MVVM: Client-Side ETL for Better Visualizations

Learn how to build better data visualizations by treating charts like MVVM components with proper client-side ETL pipelines. Stop fighting your charting libraries and start feeding them clean, predictable data.

typescript

Caching Isn’t a SaaS Product, It’s a Data Structure

The industry won’t tell you this, but a hashmap does 90% of what you need

typescript

The Ultimate Tool for Managing Types in Monorepos

gRPC Is the Secret Weapon Your Monorepo Desperately Needs

typescript

Divide and Conquer Timeline Data with Typescript

Typescript time series and Date objects

ai

The Growing Importance of SMEs in AI Agent Design

AI agents are revolutionizing industries, but here's why they still need us more than ever

engineering-culture

The Myth of the “Universal Language” for Internal Tool Development

We've all heard this story before. You finally get buy-in to build a tool that solves your pet peeve. You have a plan figured out, but then your manager says the dreaded phrase, “Use a different language so that others can contribute”… which seldom happens.

react

The Performant Interface Dilemma: Taming Object Equality in React

A comprehensive guide for developers on handling object equality issues in JavaScript, with a focus on practical solutions for React applications

startup

Will My Startup's Problem Be Big Enough?

How to evaluate the potential of a startup idea by understanding your Serviceable Obtainable Market, the Venn diagram of opportunity

ux

UX Meets Database Design, a Match Made in Heaven

Putting UX at the heart of user-centric SQL schema data modeling

ai

I Stuffed TensorFlow.js Into a React App

Here's what I learned about Web Workers

ux-research

Social Distanced UX Research Strategies For your Next iOS App

COIVD forced us to stop using in-person UX research. Here are some tried and true methods we're keeping after the lockdowns lift

flutter

How to Deploy Flutter for Web Apps with Netlify

Have you ever wanted to turn your iPad or iPhone app into a website?

All posts →