Better Programming

Advice for programmers.

Follow publication

Three Core Principles of Decoupled Applications

Daniel Niland
Better Programming
Published in
9 min readDec 21, 2022

Photo by Sahand Babali on Unsplash
Table of Contents

Encapsulate Business Logic, Not State
Separate Message Passing from Business Logic
State Should be Immutable

I’ve Made So Many Mistakes

Of course, through every iteration over the years, I aimed to improve my craft. I’d proudly solve some prior issues, but others just as serious would always pop up to take their place, stubborn as boogers on my code finger.

These issues were usually centered around state. No matter how carefully I partitioned all my controllers and managers, there was always some unaccounted bit of code over there that needed access to the state. These special cases slowly compromised my pristine design, leading me yet again down the tight and coupled path of the Controller Manager Muddle.

The definition of insanity is repeating the same action and expecting different results. Eventually, I was ready to stop the madness. I was ready to try something different.

Looking for an Alternative

I explored every paradigm I could find. I picked apart successful libraries looking for clues on how to make a better player.

Eventually, I convinced my team to embark on a completely different architecture for our next player iteration. It was a huge risk. We were flying blind with nothing but ambition and a stubbornness born of desperation. Failure was not an option.

And we succeeded. We hit it out of the park in record time. The player we designed was robust; we went for years without escalations around the core functionality. It was scalable; we added features without coupling. It was extensible; we had other teams from other companies modifying the player without a problem. Furthermore, our team of four to six engineers was more productive than other teams of twenty.

Alas, this player and its innovative design have not survived the whims of corporate dismemberment.

Therefore, I’m writing these articles to pass on some of my team’s hard-earned learnings even after the code is no more.

Principle #1: Encapsulate Business Logic, not State

What would happen if you were to centralize every piece of long-lived state, just take them out of your classes?

We had to ask ourselves this question about our new player design. We were going to create some variation of Redux but on a grand scale. We believed that having one easily accessible source of truth would solve a huge number of problems.

But take out the state, and what were we left with?

Honestly, just a bunch of business logic that didn’t make sense because it was smeared across the application like dog poo on a Parisian sidewalk. (Actually, Paris has really cleaned up its poop-on-the-sidewalk problem, but I still remember walking down those streets in the nineties.)

And then it hit me like a diamond bullet right through the forehead. It’s the business logic. The BUSINESS LOGIC.

The. Business. Logic.

State is just communication between different bits of business logic over time.

We’ve been encapsulating the wrong thing all along.

Gnomes of Different Stripes

Photo by David Brooke Martin on Unsplash

Imagine that we switch out our computer program with a bunch of gnomes sitting at desks in a room.

In one room, we’ll call it the Reactive Room (foreshadowing… check), you give each gnome a set of tasks. Every few minutes or so, they receive a piece of paper in their IN box that gives them all the data they need to finish those tasks. They make their computations (their Business Logic). They drop their changes into the OUT box. Done.

In the Reactive Room, every Gnome has one job. Business Logic is green. (Remaining images by author)

It doesn’t matter to any gnome where the data in the IN box comes from or where it goes after they put it in their OUT box. A reactive gnome doesn’t need to be in the same room as the other gnomes: when they’re being trained or tested, they can be handed arbitrary pieces of paper to ensure they know what they’re doing.

Also, Gnomes are known to cause trouble. They’ll slow down for no reason, or worse, they’ll steal your donuts. In the Reactive Room, there is no tolerance for slow donut-stealers; problem Gnomes are immediately replaced.

Right next door, things are not so orderly in the Oriented Object Room. Each Gnome keeps its own sheet of paper full of cryptic notes. They don’t willingly share these notes out of fear some capricious scribbler will change the data unexpectedly, so they set up a series of IN boxes. If other Gnomes need to do something with their data, they need to ask nicely.

The Object-Oriented gnome has to be good friends with all his gnome co-workers.

The task lists for these Gnomes can be disjointed and cryptic, involving a lot of figuring out what the other Gnomes around them are doing. This often means they’re executing small bits of larger tasks. Even if they understand their own part, they may lose sight of how their work affects that Gnome three desks down who keeps sending frantic error messages that no one else understands.

To top it off, you notice one of the Gnomes munching on your bear claw.

When you threaten to replace them, that gnome smirks and says, “You don’t know what I do.”

Your bear claw.

So, which room would you rather eat your donuts in? Hopefully, you don’t have to think too hard about that.

Principle #2: Separate Message Passing from Business Logic

You want to be in the Reactive Room, of course. It’s much easier to manage.

Your gnomes don’t need to know how data gets into their IN boxes, and they don’t have to worry about what happens to that data after they lay it into their OUT boxes.

Flying pixies could flit from desk to desk, or the messages could be wrapped in flaming arrows; it doesn’t matter.

But wait, how do we decide which messages go where?

Let’s back away from the gnome analogy and look at an actual decoupled message-passing system. Let’s look at Redux.

In Redux, component code that wants to kick off specific business logic will dispatch an action along with the relevant data for that action to be carried out. That action gets routed to a specific reducer, which is simply a function that takes the data from the action, executes some business logic over the current application state, and returns a new application state.

Redux. Business Logic in green box.

This scheme works well with React or even Angular, but Redux must be extended to handle more complicated use cases. As such, it isn’t a great fit for an application with a complex state.

But, the concept of an action is extremely interesting.

Actions speak louder than events.

To be clear, actions are not events, though they are cousins. An event lets you know something just happened, e.g., a video was paused. An action lets you know something specific is ready to happen, e.g., it’s time to pause the video.

You can think of an action as a contract between the code that dispatches the action and the code that receives it: the data necessary to do your job has been set correctly — take it away.

We can use actions as the basis for hooking together complex business logic.

We can use actions to stream our state through the business logic of our application.

This business logic is not in a reducer. These are different beasts. This code must consume and dispatch actions for the state to move through the application.

There happens to be an industry standard name for this type of code.

Actors

I hope I’m not surprising too many people here: I’m talking about the Actor Pattern from Reactive Programming. Hey, I foreshadowed it and everything.

You can think of Reactive Programming as stream programming. The work of one Actor flows into another Actor, then another, creating a pleasant rushing river of state through your application.

There is a huge amount of flexibility in Reactive Streams.

You can have one actor conditionally dispatch different actions, as shown in the diagram below:

Streams may be split to handle different sets of business logic.

Or you can implement state machines or enable timers that kick off other Actors.

Also, you’re not limited to one Actor per action. You can string them together like this:

You can build ways for Actors to be used together, such as passing deltas along with the state.

There are any number of use cases where multiple Actors interact with the same actions. You could have a race, where the first Actor done wins, or a parallel execution where multiple Actors, or even multiple instances of the same Actor, work together to split up a task.

The Akka library for the Java runtime provides a robust suite of these connectors. Typically, however, such libraries are geared toward backend systems. I’m unaware of anything similar that’s workable in JavaScript (though I’d love to hear if there is something out there). For my own application, I created a fairly simple DSL that was good enough.

I’ll write more about implementation in the future, but the main takeaway right now is this:

  • Because message passing is decoupled from Actors, if the needs of the application change, Actors can be used in different ways without changing their implementation.

This flexibility to move around business logic is a huge strength of Reactive Programming and the Actor Pattern. It allows stateful applications to scale over time.

Object-Oriented Design? Not so much.

Principle #3: State Should be Immutable

Who knows what data an Actor will need to work its business logic magic? One of the stark realities of developing a complex application is that it’s very difficult to predict how and when state will be used.

Therefore, we should allow Actors to access all the state. Yes, all of it, even stuff that doesn’t seem relevant at first. Why limit your future self? We’re working on the client side; there’s no extra overhead in passing around a reference to a large object.

But, to allow an Actor to access all the state, we need to set limits. The state must be immutable. Actors can read any state they want but can’t directly overwrite it. That would be unpredictable and very untyped of us.

We also don’t want the Actor to take on the burden of modifying the state, as that couples the state transformation to the business logic. That may be fine in a Redux reducer, but it doesn’t scale well.

We use a delta object to handle the modification.

Only the State Store can mutate the state.

This delta object can be typed and associated with a specific action. This allows for compile-time or runtime validation.

A central State Store allows us to swap out the mutation process. Using seamless-immutable but want more speed? Upgrade to immutable.js, and no other part of your application needs to change.

The Moral of the Story

These three principles are reflections of each other. You can’t encapsulate your business logic without decoupling that business logic from both state and message passing.

OK, you can. You can try, at least. In my own experience, half-measures have become weaknesses in the system that my team has to work around.

But hey, I forgive myself. Like I’ve said, I’m no hater. All software must be refactored or eventually thrown out.

What matters is that the next version we make is better, that we don’t keep repeating the same mistakes over and over and calling it Object-Oriented Design.

If you’re an astute reader (and I know you are), you’ll realize that these three principles aren’t enough to make a working application. In my next article, I’ll add the context around Input/Output and Side Effects — the reason we make applications in the first place.

Until then, happy coding!

Daniel Niland
Daniel Niland

Written by Daniel Niland

Studying the intricacies of life and video players.

Responses (2)

Write a response

When you threaten to replace them, that gnome smirks and says, “You don’t know what I do.”

This is brilliant. A good belly laugh. And sooo true!

1

It is unclear to me what the author is trying to suggest. On the one hand, he says state are immutable and on the other they can be changed via other functions.