Skip to main content

Remove temporal dependencies ⏰

An alarm clock in chains.

I want to talk about temporal dependencies. While it sounds complicated, it is really just a fancy way of saying that “these things must be done in the right order”. And you can make your code better by removing them.

Forget this and your program will crash #

Let’s look at an example. Say we have an API with two functions that users are interested in calling, creatively named doThis and doThat. However, both of these require some data to be read first and that happens to be a very slow process.

We have an init function which takes care of preloading this information, so the actual operations can access it instantly. Having init as a separate function is good since it clearly communicates to the user that there is some initialization process that needs to be done, and they can control at what point they want to perform this expensive operation.

The code in our JavaScript example looks like this.

let sharedState = null;

function init() {
  sharedState = verySlowRead();
}

function doThis() {
  if (sharedState === null) {
    throw new Error('init() must be called first.');
  }
  console.log(`This performed with data: ${sharedState}`);
}

function doThat() {
  if (sharedState === null) {
    throw new Error('init() must be called first.');
  }
  console.log(`That performed with data: ${sharedState}`);
}

Using this API is quite straight forward, as long as you remember to call init!

init(); // Forget this and your program will crash
doThis();
doThat();

What we’ve got here is a temporal dependency between init and the doThis and doThat functions. You must call the functions in a particular order, and it is up to you to remember doing so. Otherwise you will be punished by a runtime error.

Enforce the temporal dependency in the design #

The problem above is not really that init must be called before one of the other functions. That is just an effect of that the data we need is very slow to access. The actual problem is that the dependency between the function is not visible in the API design. You have to read the code or documentation to find that out.

To improve the situation, we can shape the API to make it impossible to even call one of doThis or doThat without having called init first. One solution is to make the init function return (an object containing) the doThis and doThat operations.

function init() {
  const data = verySlowRead()
  return {
  
	doThis: function() {
      console.log(`This performed with data: ${data}`);
    },
    
    doThat: function() {
      console.log(`That performed with data: ${data}`);
    }
    
  };
}

This modified API guides us through the sequence of calls. We have to call init to even be able to call doThis and doThat.

const operations = init();
operations.doThis();
operations.doThat();

Not only the interface is improved, but the implementation is simplified too. There is no longer a need for runtime checks or throwing errors, and we can make the shared state immutable ( const) instead of mutable (let).

Embed constraints in the design #

The technique of embedding temporal dependencies showed in this example is quite powerful, and goes beyond just simple init functions. And the idea of removing the possibility for error by embedding constraints is even more broadly applicable.

Some other examples include:

  • Strong typing lets the compiler remove lots of potential errors if you just tell it what type things are supposed to be.1
  • Encapsulation in object-oriented programming forbids you to directly access the internal state, only exposing a few controlled operations.
  • Immutable data structures makes concurrent modification impossible when sharing data in a multi-threaded system.

But temporal dependency can often be extra tricky to discover, as it is not clearly visible in your code. The next time you find that there is a dependency in the order which things must be called, see if you can avoid that temporal dependency. Consider how you can change the design to make it impossible to do it wrong!


  1. Kill two bugs with one type discusses a case wheretypes can help avoid common types of bugs in your system. ↩︎