The art of programming is, at its core, the art of managing data. Take the Facebook app, for example: there are user posts, direct messages, friends lists, friend requests, and more. But does it really matter what the "Send" button in a chat looks like? Does it matter whether you scroll posts vertically or horizontally? Or whether the notification indicator is on the left or the right?
On one hand, details like these do matter. On the other hand, it's easy to see that they're just surface-level concerns. If we strip all of them away, what remains is the essence of any application: data, and how we manage it.
For example, when you send a message to a friend, two things happen:
- A network request is sent to an API to save the message in a database.
- The message appears at the bottom of the chat screen.
In both steps, we're simply taking data and doing something with it—sending it or showing it. That's what programming boils down to: managing data.
So how does state management relate to this? Well, "state management" literally means "managing the state of data"—or just data management. These two terms are essentially interchangeable.
But most developers never make this connection. When people hear "state management," they often think of business logic, or a "glue" layer that connects the UI and the logic. Because of this, state management is often treated as an afterthought. Why care about the "glue" if it doesn't seem important?
But if we replace the term "state management" with "data management," its importance becomes obvious. You can't afford to treat your data as an afterthought. It's the foundation of your application.
Building applications is hard—especially when multiple teams and hundreds of thousands of lines of code are involved. In my opinion, one of the biggest reasons it's so difficult is that nobody handles data management properly. And as I've said, data management is the most fundamental part of software engineering.
So, why was this article written? In my 10 years of programming, I've never seen a team, a developer, or even a single article that gets state management right.
Today, we're going to talk about the most fundamental mistake in state management: unpredictable states—a problem that arises when logic and state are mixed together.
What is the problem?
To understand the problem, let me explain with an example. Let's say we're building a chat application. The app has chat rooms, and each chat room has a list of messages. When a chat room is opened, we need to download the latest messages and display them to the user.
Here's how state management is usually done in this case:
When a user lands on the page, some kind of state method is called:
chatRoomState.getLatestMessages();
-
Inside this method, there is typically a use case or a controller:
class ChatRoomState { getLatestMessages() async { final downloadedMessages = await GetChatRoomMessagesUseCase.call(); // Set the new state and notify all listeners. state.latestMessages = downloadedMessages; } }
-
Inside the use case, we usually call a repository:
class GetChatRoomMessagesUseCase { call() async { return await getChatRoomMessagesRepository.call(); } } class GetChatRoomMessagesRepository { call() async { return await http.get('/chat-room/123/messages'); } }
Here's a diagram of that process:
Note: if you're unfamiliar with use cases or repositories, think of them simply as classes that perform specific actions. In this example, state methods call classes, which in turn call other classes. There's a clear nesting of responsibilities.
So, what's the problem?
Let's say we're asked to show a "Messages loaded successfully" snackbar after the messages are downloaded. Whether or not it's good UX is not our call—we just need to implement it.
Now the question is: where should this code go?
Putting the snackbar inside the ChatRoomState
doesn't feel right—snackbars and chat room data shouldn't be mixed. Adding it to the repository is definitely wrong, since repositories should only handle one responsibility. So maybe we should place it in the use case?
That sounds reasonable at first:
class GetChatRoomMessagesUseCase {
call() async {
final messages = await getChatRoomMessagesRepository();
Snackbar(message: 'Messages loaded successfully').display();
return messages;
}
}
At first glance, it looks fine. But problems arise quickly.
Imagine we now need to load messages on a second page, but with a specific requirement: don't show a snackbar on success. Meaning, sometimes we want to suppress the snackbar and sometimes we want to show it. So we add an argument to control that:
class ChatRoomState {
getLatestMessages({bool? isSnackbarHidden}) async {
final downloadedMessages = await GetChatRoomMessagesUseCase.call(
isSnackbarHidden: isSnackbarHidden,
);
state.latestMessages = downloadedMessages;
}
}
class GetChatRoomMessagesUseCase {
call({bool? isSnackbarHidden}) async {
final messages = await getChatRoomMessagesRepository();
if (isSnackbarHidden == false) {
Snackbar(message: 'Messages loaded successfully').display();
}
return messages;
}
}
As you can see, the code is already getting more complicated. And our example only has two layers. In real projects, it's not uncommon to have three or four layers of nested classes.
But this isn't even the worst part.
Imagine months go by, the app grows, and a new developer joins the team. They're tasked with loading messages on yet another page. They use the ChatRoomState.getLatestMessages
function without reading the underlying implementation. Everything looks good, and they mark the task as done. But somehow, the page displays a "Messages loaded successfully" snackbar.
They're confused. This wasn't expected. But the root cause is a hidden side effect.
Think about it: ChatRoomState
is supposed to be responsible for chat room data only. And yet, using it triggers a completely unrelated UI change—a snackbar. In other words: working with chat room data unexpectedly affects the UI.
These kinds of issues are hard to detect and take time to:
• notice,
• debug,
• and fix.
You might think this is a rare or exaggerated case, but it happens more often than you'd expect.
Let me give you a real-world example from a project I worked on.
The app had three major features. Over time, our analytics showed that one feature was being used far more than the others. So we shifted focus and began redesigning the app around it. The other features were put on hold.
Months later, we discovered the truth: the feature wasn't actually being used that much. What happened?
One of our shared state methods was producing a hidden side effect. Every time any page was opened, it triggered an analytics event saying "Feature X was used". This inflated the usage stats and led to major business decisions based on false data.
This simple side effect cost us months of work and untold losses.
The solution
So how do we fix this?
By extracting logic out of the state.
We change this:
Into this:
First, let's rewrite our state:
Step 1: Rewrite the state
class ChatRoomState {
setMessages(messages) {
state.latestMessages = messages;
}
}
Step 2: Move the logic into the use case
class GetChatRoomMessagesUseCase {
call({bool? shouldDisplaySnackbar}) async {
final downloadedMessages = await getChatRoomMessagesRepository();
chatRoomState.setMessages(downloadedMessages);
if (shouldDisplaySnackbar == true) {
Snackbar(message: 'Messages loaded successfully').display();
}
}
}
In the end, everything that needs to happen for a specific action—like downloading chat messages—is placed into a single container: the use case. This makes the action easy to read, understand, reuse, extend, test, and maintain. If you ever need to change how messages are loaded, you simply go to the use case and make your changes—no need to dig through scattered classes.
Need to sort the messages? Replace the snackbar with something else? Add analytics tracking? Switch to a different data source? All of that becomes straightforward, because you know exactly where to look. And you can be confident that making a change won't accidentally trigger unrelated behavior.
What happens in the use case is what happens in the UI. No hidden side effects, no surprises, no accidental breakage.
This is what I call predictable state management.
Rules of Predictable State Management
To implement and make the most of Predictable State Management, let's define a few essential rules.
Don't create a state unless you need a single source of truth
Many developers don't know—or have forgotten—why state management was invented in the first place.
Let's say you're working on a Facebook-like website. Every few minutes, you fetch data about how many friend requests the user has. This number is displayed in three different places:
- In the top-right corner, where the notification icon shows the count of friend requests.
- In the left-side menu, next to the "Friends" link.
- On the "Friends" page itself, inside the main content area.
So, what's the problem?
Every time the data is refreshed, the UI needs to be updated in three separate places. This quickly becomes complex and unmaintainable because:
- You need to know what page the user is on.
- You need to know how many places are displaying that data.
- Each spot requires its own update logic or script.
To solve this, state management was introduced. The idea was simple: UI elements should listen to shared data sources. When the data changes, the UI updates automatically.
This gave developers a single source of truth. Instead of manually updating each UI element, you update the data once, and everything stays in sync.
That's it. Nobody set out to create a fancy "state as glue between UI and logic" philosophy. We just wanted a clean way to reuse data across the UI.
There's one important clarification: "single source of truth" does not mean "only create states that are used in multiple places". Even if a piece of data is used in just one place, managing it via state is still valid—because it might be needed elsewhere in the future. Using a state makes it easy to extend usage without major rewrites.
The real rule is: don't create unnecessary state.
Here's an example of unnecessary state:
// BAD: Do not do this.
class AnalyticsState {
trackAnalyticsEvent(event) {
AnalyticsService().trackEvent(event);
}
}
This is not a real state. It doesn't hold or modify any data. Subscribing to it would provide no benefit. In this case, you should just call AnalyticsService
directly—there's no need to wrap it in a state class.
1 data point per state — plus optional metadata
One of the most common mistakes in state management is creating a separate state for each page of the application. Developers often lump every single piece of data used on a page into the same container.
Let's look at an example:
// BAD: Don't do this.
class MainPageState {
User viewer;
List<ChatRoom> chatRooms;
bool isSideBarOpen;
bool isCreateChatRoomDialogOpen;
// Setters/getters/methods below...
}
Why is this a bad idea? There are several reasons:
It makes data non-reusable.
For example, if you want to open the "Create Chat Room" dialog from somewhere other than the main page, you'd have to duplicate theisCreateChatRoomDialogOpen
flag in another state. This leads to code duplication and violates the DRY principle.-
It breaks SOLID principles.
- Single Responsibility Principle: The state now has multiple reasons to change. If you want to modify viewer data, you edit
MainPageState
. If you want to change how the sidebar opens, you edit the same class again. - Open/Closed Principle: Adding a new feature or data point means editing this class, rather than extending functionality with new, focused classes.
- Single Responsibility Principle: The state now has multiple reasons to change. If you want to modify viewer data, you edit
A better approach is to extract each meaningful piece of data into its own dedicated state:
class ViewerState {}
class ChatRoomsState {}
class SideBarState {}
class CreateChatRoomDialogState {}
This leads to a key insight:
A good state holds a single, small, but meaningful piece of data.
What does "small yet reasonable" mean?
Let's take the example of the currently logged-in user. It makes sense to have a ViewerState
that holds a User
object. This is a single logical unit of data.
However, adding something like userHasOpenedLoginDialog
to ViewerState
would be wrong. That piece of UI-related metadata belongs in its own state. When we think about a "user," we think about their name, email, ID—not whether a dialog is currently open.
On the other hand, splitting ViewerState
into ViewerFirstNameState
, ViewerLastNameState
, etc., would also be a mistake. Over-fragmenting a small object into many microstates provides no benefit and introduces unnecessary complexity.
To be clear: I'm not saying that objects should never be broken down. If an object grows too large, you should break it into smaller parts. But in cases like this, a single ViewerState
is perfectly reasonable and more maintainable.
Optional additional metadata
As the previous rule mentions, we also have the option to include additional metadata in a state. But what exactly is metadata?
Metadata is simply "data about data". In practical terms, when we load some data, we often want to track information about that process:
Is it currently loading? Did it fail? How many times have we retried it? When did the request start?
Let's look at an example:
// A state that includes both data and metadata.
class ChatRoomsState {
// Metadata
bool isDataLoading;
Error? loadingError;
bool hasLoadingFailed;
bool hasLoadingBeenRetried;
num amountOfLoadingRetries;
DateTime loadingRequestedAt;
// Actual data
List<ChatRoom> chatRooms;
}
It generally makes sense to keep metadata as close to the data as possible. That way, everything related to a particular state lives in one place. However, if your app architecture calls for it, you can extract metadata into a separate state—it depends on what makes the most sense for your use case.
You can also improve the example above by grouping the metadata into a nested object. For example:
class ChatRoomsState implements LoadableState {
List<ChatRoom> data;
ChatRoomsStateMetaData metaData;
}
class ChatRoomsStateMetaData {
bool isDataLoading;
Error? loadingError;
bool hasLoadingFailed;
bool hasLoadingBeenRetried;
num amountOfLoadingRetries;
DateTime loadingRequestedAt;
}
Whether this structure is better is subjective. It's up to you to decide if the separation makes your code more readable or maintainable. Use it if it fits your style or project needs.
State methods must set or return data. No side effects of any kind
As we discussed earlier, doing any side effects inside state methods is a bad idea.
When it comes to classes and restrictions, it's actually not uncommon to set healthy boundaries. For instance, it's very common to use the Repository pattern to deal with remote data. Here's an example of a repository:
class ChatRoomsGetterRepository {
List<ChatRooms> getChatRooms() {
final data = http.get('/api/chat-rooms');
return data;
}
}
This class does only a single thing: it downloads remote data. It doesn't do anything else. It doesn't record analytics data, it doesn't modify the UI. It's basically an abstraction, a contract. We don't know how it downloads the data or where it gets it from. We only know that if we use this class, we will receive a list of chat rooms.
This repository is useful because it encapsulates logic and allows us to replace the data source without too much hassle.
Let's say we decided to migrate from REST to GraphQL. We can either rewrite the logic of ChatRoomsGetterRepository
or we can create a ChatRoomsGraphqlGetterRepository
with the same interface and simply replace the class. In the end, changing the source of chat rooms will not break anything else in our app.
This pattern is commonly used and its usefulness is undeniable. In my opinion, good states should be treated the same as repositories. These are classes that do very little: they hold and modify a small amount of data. We don't know how exactly they're doing it and we don't care. We only know that they cannot do anything else, and when we call state methods, a certain piece of data is going to be updated. They are simple and obvious.
Let's consider an example:
// BAD STATE. Don't do this.
class ViewerState {
User? viewer;
login() {}
logout() {}
}
What is wrong with this state, you may ask? Well, for instance, "logout" assumes that a lot of actions are going to happen when this method is invoked: a network request will be made, cache will be cleared, local storage will be erased, an analytics event will be recorded, most of the UI will be updated.
On the other hand, this snippet:
// GOOD.
class ViewerState {
User? viewer;
setViewer() {}
unsetViewer() {}
}
does not have such assumptions. When you invoke "unsetViewer," it's obvious that only the state is going to change. The logged-in user object is going to be removed from the state. That's it. There are no side effects. No complications.
This state is simple, obvious, and predictable.
Another way to think about states is to treat them as micro databases. Imagine a database that can only hold a very limited amount of data (a primitive, an object, or a list of objects). This database can only hold and modify its data. A database cannot and should not make API requests, update other databases, or force the UI to display notifications.
The less logic there is in a state, the better
Note: a quick reminder. "Logic" in most cases means "business rules".
It's very tempting to put logic inside your states. But by doing so, you might end up with the same problems as with side effects: by trying to reuse code in an improper place, you will eventually shoot yourself in the foot. Too much logic in states makes them unobvious and unpredictable.
Let's say we need to display to the user the number of participants in a chatroom. Please consider this code snippet:
class ChatRoomState {
int getNumberOfParticipants()
}
Let's say the number is 101. But inside the settings page of this chat room, we also must display the number of participants. Though this number must be lower if the currently logged-in user is the creator of this chat room. Therefore, on the main chat room page, the number of participants would be 101, but on the settings page, the number would be 100. Such is the business requirement.
This means that even though we are using the same data in two places of the application, that data must be slightly different in one of them. We must add a special check somewhere to change the number of participants. The question is: where would you put it?
You might be tempted to change the state and add a special method:
class ChatRoomState {
int getNumberOfParticipants()
int getNumberOfParticipantsWithoutAdmins()
}
Or you might want to add a flag to an existing method:
class ChatRoomState {
int getNumberOfParticipants(bool isNumberOfAdminsRemoved)
}
Sadly, both of these solutions would be wrong because changing the state to fit a special use case opens the door to unobvious and unmaintainable states. For example, let's say after a while we receive new business requirements and now we need to display 0 participants for chatrooms that are marked as "private"? In that case, we will have to somehow check if the logged-in user is a member of the chatroom and if they should have access to this information. Since previously we were putting the logic regarding chat room participants inside the state, we will have to continue doing so. With time, the state will become more and more bloated and less and less predictable.
Just to clarify: this rule does not mean that adding new methods to states is completely prohibited.
Let's say we have a state that is responsible for the list of downloaded chat rooms:
class ChatRoomsState {
List<ChatRoom> chatRooms;
update(updatedChatRoomsList) {}
}
In many parts of our application, we push new chat rooms to this list: after we created a chat room, after we were invited to the chat room, or for other reasons. In that case, it's quite handy to add a special method for it:
class ChatRoomsState {
List<ChatRoom> chatRooms;
update(updatedChatRoomsList) {}
addChatRoom(newChatRoom) {
this.chatRooms.add(newChatRoom);
}
}
You might be wondering: what is the difference between the first example with the number of participants in a chat room where adding methods is bad and the current example? Well, it's simple: the first example adds business rules to the state, the second one simplifies the way we interact with the state. The second one is a shortcut for this:
// Before:
newChatRoomsList = chatRoomsState.chatRooms.add(chatRoom);
chatRoomsState.update(newChatRoomsList);
// After:
chatRoomsState.addChatRoom(chatRoom);
Even though the first example and the second one technically both add "logic" to states, the second one simplifies our life and the first one will eventually make our life harder.
In conclusion: whenever you are unsure what to do, remember this rule—the less logic there is in a state, the better. Ideally, there should be a single "update()" method and nothing else. Though if it makes sense for your use case, do add new methods.
More complicated example
Simple examples are good to understand the basics of the idea. But what is often missing is something more complicated to make sure you can apply the idea in practice. Because reality is always far more complicated and always forces you to reinvent the wheel.
Let's say you were hired to work on a Twitch.tv-like website. On this site, you can watch people stream games, you can participate in the chat, subscribe to the streamer, and so on. Your first task is to add the ability to donate money to a streamer you are watching. How would you go about doing that task?
Task requirements:
- The number of dollars in the wallet must decrease.
- The number of experience points must increase. (Context: users of our app can level up their profiles based on certain actions)
- The chat must display "X donated Y$ to the streamer" announcement.
- These changes to the UI must happen BEFORE network requests are fired/completed (this technique is called "optimistic UI").
- A "Success!" snackbar must be displayed if network requests were successful.
- A "Something went wrong." snackbar must be displayed if network requests were unsuccessful.
- All of the UI changes must be reverted if the API responded with an error.
- Donation success or failure must trigger an appropriate analytics event.
Before we jump into the implementation, let's take a look at how developers usually approach the development. Let's read code that was written via states that contain logic:
// BAD. DON'T DO THIS.
class DonationsState { // <= First problem.
donateToStreamer(amount) async {
try {
globalLoadingIndicatorState.setIsLoading(true);
await walletState.subtractFunds(amount); // <= Second problem.
await viewerState.addExperience(10); // <= Second problem.
await activeChatState.announceDonation(); // <= Second problem.
await submitDonationRepository.submit(amount);
analyticsTracker.trackDonationToStreamer(amount);
snackBarService.displaySnackbar('Success!'); // <= Third problem.
} catch (error) {
walletState.addFunds(amount); // <= Fourth problem.
viewerState.subtractExperience(10); // <= Fourth problem.
activeChatState.removeDonationAnnouncement();
analyticsTracker.trackDonationToUserFailure(reason: error.message);
snackBarService.displaySnackbar('Oops, something went wrong');
}
globalLoadingIndicatorState.setIsLoading(false);
}
}
Descriptions of problems:
1) A state without data defeats its purpose. What is the point of subscribing to it if it doesn't have any data? And if you don't plan on subscribing to it, then it's not a state.
2) These state methods can internally call other state methods. When you read this code, you cannot be sure that the app will do exactly the things you want it to do. The behavior is unpredictable.
3) Can you be sure that this service doesn't internally change states? Pretty much the same problem as in the previous point but with more steps.
4) Without reading the implementation of these methods, can you be sure that they will do what you want of them? "walletState.addFunds" implies that we are giving money back to the user (we are increasing the amount of numbers in the UI in front of their eyes) because for whatever reason the network request failed. This means we only want to change the state (the state properties) without any side effects. We don't want any HTTP requests done, we don't want any extra snackbars to pop up. Are you sure that this method will do only that? And can you be sure that somebody won't change the implementation in the future? I highly doubt it.
As you can imagine, this code will be hard to maintain and you're always going to have a feeling that anything can break at any point in the future. You can't rely on it. It's unpredictable.
Let's take a look at the code where logic was extracted from the states. Here's how Predictable State Management looks in practice:
// GOOD. DO THIS.
class DonateToStreamerUseCase {
donateToStreamer(amount) async {
try {
globalLoadingIndicatorState.setIsLoading(true);
activeChatState.announceDonation();
walletState.subtractFunds(amount);
viewerState.addExperience(10);
await subtractFundsRepository.subtract(amount);
await addExperienceRepository.add(amount);
await submitDonationRepository.submit(amount);
analyticsTracker.trackDonationToStreamer(amount);
snackBarService.displaySnackbar('Donation successful!');
} catch (error) {
walletState.addFunds(amount);
viewerState.subtractExperience(10);
activeChatState.removeDonationAnnouncement();
analyticsTracker.trackDonationToUserFailure(reason: error.message);
snackBarService.displaySnackbar('Oops, something went wrong');
}
globalLoadingIndicatorState.setIsLoading(false);
}
}
As you can see, we did two things here: 1) we turned the class from a state into a use case and 2) we extracted all repositories from states. By doing so, we gain predictability and control over our code. And the ability to have total control over what happens in your app is the main point of this article. With this new way of writing code, you will be able to introduce any kind of changes. You want to make sure that the user has enough funds by making a special request to the API? Not a problem. You want to remove the global loading indicator and instead use the "isLoading" property of walletState? Sure, not a problem. You want to display errors by using activeChatState instead of a snackbar? You can do so by rewriting 4 lines of code instead of rewriting 4 methods.
Even though predictability is amazing, the only downside of this approach is it's harder to reuse code now. Let's say that each time we award the user with experience, we also want to check if the user has enough points to level up. If they do, we want to display some kind of leveling up animation, track an analytics event, and make an API request.
In that case, we are faced with a dilemma. Do we place all this leveling code into a service and lose predictability and control? Think of the "Optimistic UI" requirement of our task; with a separate service, it will be hard to implement it.
How do we reuse code and keep predictability? That is a topic for our next conversation. TO BE CONTINUED.
Predictable vs unpredictable (properties comparison)
Unpredictable states:
- Contain multiple data points
- Have methods with side effects
- Can have unobvious method names
- Have no rules on how to use states
- Contain business rules of the application
- Do not follow SOLID principles. Mainly S and O
Predictable states:
- Contain only a single point of data
- Have methods that only change data of the state. No side effects of any kind
- Have obvious method names
- Have specific rules
- Act as containers for data. They do not contain business rules
- Follow SOLID principles. Predictable states have only a single reason to change and new functionality is added by adding new states instead of extending existing ones
Additional Benefits of PSM
Easier maintenance and onboarding of new developers
When logic is extracted out of states, it's usually placed inside Controllers or Use Cases. Compared to the previous approach where logic was mixed between the view layer, states, and controllers, this allows developers to more easily understand the business rules of the application.
This also makes onboarding of new developers easier and reduces the "bus factor".
Undo/redo functionality
When your states do not contain any logic or side effects, it's extremely easy to roll back state changes.
class UndoableCounterState {
counter = 0;
_stateHistory = [];
update(newCounterValue) {
_stateHistory.add(counter);
counter = newCounterValue;
}
undo() {
counter = _stateHistory.last;
_stateHistory.removeLast();
}
}
Why do people get away with unpredictable states?
Short answer:
Developers don't actually get away with doing states wrong. The reason why it feels that way is because it takes time for problems to pop up. And by the time they encounter bugs and difficulties, they are so deep down the rabbit hole that it's impossible to see solutions to fundamental problems. Quick fixes and workarounds do help in the beginning but eventually make states unmaintainable in the long run.
Long answer:
Since I claimed that most teams and companies make a lot of fundamental mistakes when it comes to working with state management, how come nobody has noticed any problems?
As with anything, there are a lot of ways to do something wrong and only a limited number of ways to do something right.
1) They don't.
Usually, it takes a lot of time to get in a situation when adding new features or fixing a bug gets frustratingly difficult. By that time, it's too hard to tell where exactly everything went wrong. And even if developers encounter such problems sooner, they simply update the state code and move on. Mind you: adding more methods to states in order to add a feature breaks SOLID principles.
2) People don't write anything complicated
Let's face it: the lion's share of applications don't encounter any problems because their lack of complicated logic allows them to get away with it. I'm not saying that most applications were easy to build. But I am saying that most of them do not deal with interconnected states, multi-team environments, and long lists of business requirements for every user action. Usually, it's as simple as: download an object, use the object to display something to the user, update the object in the state when an action occurred.
Their approach quickly falls apart when logic gets complicated. For instance, when a single action leads to a change of 5 different states and a call to 20 different services.
3) People work in small teams and/or on small projects.
When a developer encounters a problem (and all of them inevitably do as a project gets larger), it's easy to rewrite big chunks of the application if 3 people are working on it. However, when there are 20 teams and 100 developers working on a single app, then changing existing code quickly gets complicated.
4) People don't care about doing things properly. They are blind to problems.
When we encounter problems during development, we usually try to add a quick fix on top of the problem or we try to do a workaround instead. We add bad code on top of bad code instead of rewriting the bad parts. Basically, we are more focused on finishing a task and gaining short-term benefits. These benefits always hurt us in the long run.
As it was said in "Clean Code" by Robert C. Martin (and I am heavily paraphrasing here): "We don't have time to write clean code, so we write bad code that will slow us down even more in the future".
Top comments (0)