Declarative VS Imperative programming

If you don't know what this means, Then you should read this article. We explore the differences between two different paradigms and the effects they have on our code. Do you have a preference?

Declarative VS Imperative programming

If you don't know what this means, Then you should read this article. If you think this is a good article, Then you should share it with friends or colleagues. If you understand what this is, Then you should comment below with additional information if anything was missed. If you think we should move forward, Then continue reading.

What is the difference between Declarative vs Imperative programming?

First off, let's talk about Imperative programming. It is most likely the one that most people inherently understand when first learning how to write the simplest of scripts.

Define: Imperative Programming

Imperative programming is a programming paradigm that uses statements that change a program's state. ~ Wikipedia

The first paragraph of this article was written in a form of imperative statements. If-This-Then-This type statements in code are imperative. Any time you end up asking a question, you have entered the realm of being imperative.

Define Declarative Programming

Declarative programming is a non-imperative style of programming in which programs describe their desired results without explicitly listing commands or steps that must be performed. ~ Wikipedia

The best example of declarative programming are as-code paradigms. For example Docker-Compose/Kubernetes configuration files  or terraform and ansible. All of these applications take in a configuration file and create a series of steps to construct infrastructure to satisfy the declared configuration.

So why is one better than the other?

I wouldn't say that one is necessarily better than the other, but I try to prefer Declarative programming over imperative for these reasons.

Imperative programming is verbose and tedious. Especially in applications where a lot of different conditions can occur. The event-driven architecture is the thing that comes to mind that may have such conditions. An event comes in and the event can have multiple actions, created, updated or deleted. If you were you write some code to handle the different cases it might look something like this.

// Javascript/Nodejs
event.on('customer', (eventPayload) => {
  const { action, data } = eventPayload;
  
  if (action === 'created'){
    return controller.create(data);
  else if (action === 'updated') {
    return controller.update(data);
  else if (action === 'deleted') {
    return controller.delete(data);
  }
  // ignore event
});
Note: Now, I am also assuming a bit here that we don't have different event types. customer.updated might be a better event. However, if you were to build a common event handler that could handle all these actions, the problem will still exist.

Implementing the code above with declarative programming in mind may look like more code, and in some cases, it can be, but the number of additional cases can be added with minimal lines of code and sometimes even just 1 line of code.

const actionMap = {
  created: controller.create,
  updated: controller.update,
  delete: controller.delete,
};

event.on('customer', (eventPayload) => {
  const { action, data } = eventPayload;
  const operation = actionMap[action] || () => {};
  
  return operation(data);
})
Note: In this case, the code is simpler to read but it can also handle more actions by just updating the actionMap above the event handler. This ends up being hugely powerful because the number of lines of code can be boiled down to a repeatable pattern that all actions run through. Meaning this becomes the backbone of your application.

In Javascript, in particular, this pattern is extremely powerful. Given the flexibility of objects and noop functions is easy to account for missed actions and guaranteeing that the code will always run the same way no matter how many different types of actions come through for a given customer event.

Where it starts breaking down

You might have noticed, or maybe not, that if you follow this kind of pattern you can quickly let the code get out of hand for edge cases in your controller functions. Say that the delete function now requires you to pass along a groupId, where create or update don't require it. This could be a code smell that that kind of dependency should be moved out of the controller. But for sake of the argument let's press forward.

You could start implementing a contract where the controller will take in the arguments of (customerData, groupId). create, and update would just not use the groupId argument. Then say some time in the future, create needs a role for some reason. Now all of them would have (customerData, groupId, role). This is a terrible example but I hope you start to see where certain requirements may need to provoke a change.

Prioritize configuration over imperative programming

There are a lot of examples I can give on this particular topic. I see it all the time. You should aim to prioritize application configuration over needing to ask how this particular code should run.

Continuing with javascript and node, here is another example of what I mean.

if (['production', 'staging'].includes(process.env)) {
  const lib = new MyLib({ 
  	someconfig: 'does', 
    stuff: 'only-in', 
    prod: true 
  });
  
  lib.dowork();
} else {
  const lib = new MyLib({ 
  	someconfig: 'does-not-do', 
    stuff: 'only-in', 
    prod: false
  })
  
  lib.dowork();
}
Point: Changing how your code works based on the environment you're in.

First of all, don't do this because it invalidates the 12-factor app. Your application code should run the same way in every environment regardless of configuration. The only thing that should change is the configuration values or what those libraries/applications point to.

How do you fix this?

Generally, you would do this by moving your configuration into something that could be referenced on deployment and app start. Using something like environment variables so your application can pull from those to set itself up. You could also use libs like node-config or dotenv. These allow you to create configuration based on environments at which point your application should pull those in and just run with that configuration instead of asking what environment it is in. This will reduce the number of bugs that are only reproducible in a single environment. Ironically it will make all bugs show up in all environments but that is the point! At least you will be able to reproduce it on your machine and not just stage or prod.

Here is an example. First, let me show you what the config files might look like per environment:

// production.js
module.exports = {
  myLibConfig: {
    someconfig: 'does',
    stuff: 'only-in',
    prod: true,
  },
};

// staging.js
module.exports = {
  myLibConfig: {
    someconfig: 'does',
    stuff: 'only-in',
    prod: false,
  }
}
These would be separate files and based on different environments.

They don't look much different than the configuration that was being passed to the lib above, now does it? And it really shouldn't. We are merely pulling out the configuration values.

Next, this is what your new lib would look like:

const config = require('config');

const lib = new MyLib(config.myLibConfig);

lib.doWork();
For this example I am using node-config.

As you can see, there is a lot less code here. This is because we have moved the conditional logic of determining what lane we are in, and instead to application start. In both examples you are specifying the configs the same way, the difference is depending on the lane. Only one config is used by the app at any given time. How succinct and declarative 😉.

That's a wrap

So that's it. There isn't much to this pattern but it can be extremely powerful. Powerful, yet simple.  I love this pattern because it promotes commanding your application to act. Instead of needing to constantly be asking what state you're in and then realizing you need to run something.


This thing has happened.

Go. Do the things I say.


Keeping this pattern in mind can also help you set yourself up to move over to things like containers with docker. You've set yourself up to be able to start utilizing environment variables or applications like Consul by the very nature of pulling the configuration out of your code.