1 Comment

Save Side Effects for Last

Over time, I’ve noticed a design heuristic that has helped me immensely over innumerable projects. It’s actually quite simple: You can think of it as saving your side effects for last.

What does this mean? To illustrate, let’s consider an example. You’re working on a story that involves scheduling future user notifications. There are some inputs to consider, such as the user’s preferred hours and the passage of time. You’ve decided to schedule them out a week in advance. You’ll need to schedule new notifications periodically, but you may also need to remove or reschedule existing notifications.

A natural inclination may be to start writing something like:

for each day in the next week
  is there no notification scheduled?
    generate and schedule it now (side effects!)
  otherwise, should it be rescheduled?
    remove the existing notification (side effects!)
    generate and schedule the new one (side effects!)

In this example, we are modifying the world while we’re doing our processing. Side effects aren’t inherently bad. Actually, they’re the whole point of writing software. If your code had no effect, nobody would use it.

Still, ask yourself how you could save those side effects until the end. Ultimately, the strategy is to build some data describing any actions that need to be taken.

for each day in the next week
  is there no notification scheduled?
    add some data to the output describing a new notification
  otherwise, should it be rescheduled?
    add some data to the output describing how the notification should be removed
    and add more data on how to schedule its replacement

The output may look something like:

[{action: "remove", notificationId: 4},
 {action: "create", time: "some ISO8601 time"}]

You would then hand that data off to another, much simpler function that would perform those side effects.

Why Go Through the Trouble?

It’s true that this heuristic will lead you to some extra work, but there are many benefits. Among them:

First, it creates a natural boundary that is very useful for testing. The logic in the pseudo code above (like “otherwise, should it be rescheduled?”) can actually be quite complex.

In systems, the logic for deciding what change to effect is often much more complex than the logic for effecting said change. By drawing this boundary, you gain the benefit of writing much less arduous tests. There is less to mock, and it’s easy to write assertions for data.

Second, debugging becomes much easier. There’s something really magical about being able to ask a system what it’s going to do and receiving a full snapshot of its plan before it begins executing it. This is especially true of more complex features, such as synchronization with an external system.

Finally, this approach enables you to easily layer transformations. Using the above example, let’s say that your notification scheduler should avoid certain holidays. You could write a simple function that receives a list of scheduling actions and filters out any that occur on specific dates.

It’s Just a Way to Trick You into FP

What I like about this heuristic is that it’s conceptually easy to understand and apply, but it naturally guides you closer to functional programming.

Indeed, this is very similar to how Haskell handles IO. It’s also at the core idea of modern web/UI frameworks like React.

I hope this approach is as helpful for you as it has been for me.