Software Principle 10: Keep State at the Edges
Complexity in any system is what creates defects. Developers are easily able to reason about a single unit of code or even sometimes, a simple component. If that is the case, then why are defects understood to be part of software development?
There are two causes of this: intricate integration patterns and state.
Complex integrations can be caused by not enforcing proper boundaries.
State is the cause of almost every bug and is the ultimate creator of complexity in any codebase. State comes in an infinite number of forms and is impossible to predict or control fully.
Often, the state is user-provided, and each user will use your system in whatever way they deem appropriate creating mutations of data that a team hasn't considered. Keeping state at the edges of the system will allow you to form the state into a known acceptable form that helps reduce the complexity of your code downstream and where it matters the most: into the core of the system.
Defining “Edges” and “State”
To start, what are the edges of a system and what exactly is the state? The most explicit definition of a system edge is the parts where the context of control leaves the current system.
For example, an edge can be where the system is interacting with a file system, database or external API. An edge can also be where the context of control is passed off to another part of the system like a return API call to a user-facing mobile or web system.
The state is any data or configuration that is required to make the system work. These can be database connections, persisted data in a file or database, external URLs needed, or cached information.
Keeping State at the Edges
In this sense, what benefit does keeping state at the edges give us?
When state is stored and kept at the edges of a system, then the center or core functionality of the system is stateless; which provides many key benefits to the architecture of the system.
The first key benefit is testability. By not requiring state to test the internals of a system, techniques like mutation testing and dynamic analysis that inject state into the system can be used with ease and can provide insight into the many state-related bugs that may occur within the system.
Another key benefit is that it allows for the inversion of control at almost every level from the edges. This inversion allows for extremely reusable code and components as well as the interesting composition of objects or functions which often accelerate development times. The reason for this is no specific requirement exists for code to function and therefore many forms of state can be used to execute the function.
The third key benefit is, refactoring the system becomes much more manageable. As there are no hard state requirements and state is passed in from the edges of the system, the internals of the system can be moved and factored with ease. This is because of the flexibility created from the testability of the internals but also because the code again doesn’t rely on specific conditions and therefore becomes more general.
Ultimately what this means is that the external parts of the application are where the majority of the complexity lies, but that means the core internals remain simple and easier to manage.
Since this is where the majority of most business logic and core functionality exists, this simplicity will allow your architecture to scale much more efficiently, as well as enable developers to understand the input.
While this principle will not prevent all bugs from entering the system, combining this with enforcing your boundaries will allow your system to have clean places where state enters the system, is transformed and passed around to key components and code while not relying on mess state transactions themselves.
This, in itself, will prevent architectural issues as well as avoid many defects in any system.