Common concepts behind software design patterns

Software design patterns are tried-and-true ways to structure our code to ensure their cleanliness, maintainability, and ease of change. For Object-Oriented Programming (OOP), we have the famous Gang Of Four design patterns. For Functional Programming, it is less opaque, with ideas like immutability, currying, and higher-ordered function taking over-generalized patterns.

I want to take a higher-level view of these patterns and look at some of the ideas that come up time and time again.

Push and pull

A majority of programming can be simplified to changing data from one form to another. So many of these patterns come down to how we receive and pass data from one place to the next.

  • Push: The system knows the destination and pushes data to that location when they are done processing them
  • Pull: The system does not know the destination, but allows other systems to ask it for data

One example of this happens in reactivity systems. Push-based reactivity systems are easier to understand, as all reactive atoms react immediately to state changes to things they are subscribed to. Pull-based reactivity systems are lazy, so reactive atoms are more efficient as they only need to recalculate their value when they are needed. Compared to push-based reactivity, pull-based reactivity is more scalable, as the addition of more reactive atoms does not slow down the entire reactivity system.

Another example of this is in GitOps, a deployment pattern rising in popularity. In a push-based GitOps, servers are updated by the Continuous Integration (CI) when the git repository is updated as well. In a pull-based GitOps, servers instead pull their new state from the git repository and update based on it. Pull-based GitOps is more scalable and more robust, as the failure and slowness of the CI system do not affect the system. On the other hand, observability in pull-based GitOps is much harder, as the current state of the server is not reflected.

So although we are talking are real servers instead of reactive atoms here, similar principles apply.

New state or delta updates

Along with the push and pulling of data, patterns also defer in terms of whether updates are in the form of full state updates, or only delta changes.

One example of these is HTTP put (new state) vs HTTP patch (delta). Depending on the number of fields in the piece of data one is updating, the put operation might be easier to reason and update, which leads to fewer bugs. The patch operation sends less payload, which decreases system load due to reduced bandwidth, but it makes tracing harder as you no longer have an entire view of what the data state is like by looking at the request itself.

This principle also pops up in reactivity systems, and Conflict-free replicated data type (CRDT). For CRDTs, this single difference splits the research into separate branches.

Declarative or imperative

Most people's first programming language is imperative. It is easier to understand code as operations mutating state and observing the system as is. Imperative languages let the programmer state what steps to take to reach the end goal.

On the other hand, declarative language asks the programmer to describe what the end goal should be, and the program will execute instructions to achieve it. Most people's exposure to declarative languages is rarer, with SQL being the most well-known example.

Declarative paradigms are easier to write and understand, and as such, cause fewer bugs. In an eyes of an expert, however, the tradeoffs of performance and flexibility may not be worth it. That could be one reason why most programmers tend to avoid no-code and low code platforms.

That is not to say declarative paradigms are slow. Declarative paradigms can often simplify and abstract the difficult parts away, making it easier to utilize patterns like delta updates to make up for the performance loss.

In practice, most languages have a mix of both paradigms. The rendering library React takes the declarative approach to render HTML elements, but is imperative when you need to add states and effects for interactivity.

As another example, deploying infrastructure is used to favor the imperative pattern with the initial rise of tools like Chef and Ansible. Terraform, which was fully declarative, came later and took over the scene by storm. It became the preferred tool as Terraform made it easier to understand what the state of infrastructure looked like at the moment, which results in fewer inconsistent deployments.

Conclusion

These higher-level concepts occur over and over again in multiple places. They can be used to categorize various tools and languages we use and further our understanding. Most importantly, I hope they can be valuable when you implement your patterns and solutions. Let me know of more of such concepts on Twitter at @falconets!