Development time vs build time vs runtime

Ricky Iudica
Geek Culture
Published in
6 min readApr 16, 2022

In this article I will analyze development time, build time and runtime to answer the question of what, how and where we can configure our web application or software. And, of course, when to prefer one option over the others.

Photo by Caspar Camille Rubin on Unsplash

Introduction

Today every software development lifecycle goes through several phases, which vary depending on the programming language, the technology stack, etc. For the sake of our discussion let’s consider these three:

  • Development time
  • Build time
  • Runtime

And let’s try to answer these questions:

What aspect of our software can we configure in each phase? And how?

Development time

At development time, we can define constants, magic numbers (PLEASE DON’T DO THAT!!) or even more complex configuration, like when we instantiate a concrete implementation of an interface or service. These are examples of static (with the meaning of fixed, immutable) configurations for our software.

Physical constants are a perfect example since they never change. Another advantage of this type of configuration is that the constant is available at startup, when the class is loaded in case of class variable, or at instantiation in case of instance variable; besides, it will be kept in memory to be readily available.

Example of static constant

Build time

This is the most complex phase. Depending on the technology, we can setup a gradle or npm script with many steps or tasks. Typical tasks may include: compilation, unit tests, integration tests, obscuration of code, bundling, packaging, etc.

We can also setup more complex tasks. For, example, imagine that we have our code base and / or our configuration files distributed and stored in several locations. We could set up a task to fetch this data, recompile it and produce some artifact (a jar, json, etc..) that is loaded at startup. If the processing of these resources takes long, by doing it at build time we speed up the execution at runtime, since our system won’t need to fetch those resources anymore, having the artifact already available in memory.

The following diagram shows a simplified example of what happens: our build tool fetches the information from different servers or repositories and produces an artifact to be put in the production environment.

Fig. 1 — Build time

Runtime

At runtime our software uses the data from the user’s session to drive the business logic. Some of the variables that can be used are: preferences, language, location, permissions, etc..

Imagine, for example, that the user logs in to our software. We can use their preference or session information to adapt the UI (e.g. setting the labels in the language of the user, rendering the language in left to right or right to left, setting the permissions according to the user’s role, etc..).

The configuration of the software at runtime is dynamic. Normally this is achieved with proper usage of design patterns like factory and / or strategy patterns, dependency injection, etc.. You can see an example in this snippet:

Example of dynamic instantiation

We use the information from the user’s request, in this case the language, to instantiate dynamically the right service and drive the business logic.

In the world of Microservices, another example is constituted by a discovery service, which is accessed at runtime to redirect the user request to the correct back-end service.

Fig. 2 — Discovery service in a Microservices architecture

These examples show how the dynamic aspect of the runtime execution favors more decoupling at the expenses of complexity.

How to choose?

We saw three phases, three moments in the software lifecycle where we can configure our software. But if I have to choose, which phase is the best? Let’s take few criteria (there are much more, of course) and let’s see how each phase behaves and what we should consider to take a decision.

Static vs Dynamic configuration

At development time we set things like constants that we cannot change. On the other hand, when we shift from development time to build and then to runtime, we gain more flexibility at the expenses of more complexity.

Of course in a real project we will have a mix of the three types of configuration: there will be simple algorithms that can be configured with simple constants and there will be more complex scenarios which requires a more dynamic logic, like in the controller example.

So, here the decision is taken on the basis of whether the configuration of our logic changes with time, always compromising with the complexity we need or are willing to have.

Execution Time

This aspect marks the difference between development and build time on one side and runtime on the other. Let’s imagine that some data in our software is used for our business logic; for example a table with all the time zones (just made it up while writing). We could have this information:

  • stored as constants (development time);
  • recompiled into a json after fetching the data from a time server and then loaded in memory (build time);
  • stored in a time server or in a DB that we access at runtime at each request.

As you can see we have different alternatives. But: do we have strict requirements in terms of response time? If so, then we should prefer to have the data pre-loaded in memory somehow. If this is the case, we would prefer the first or second options. As counter example, say that the data is not immutable and can be updated; in this other scenario, we would go for the third option and optimize its behavior, eventually refactoring the logic to load at startup the data from the server.

Memory

In the previous point we looked at how pre-loading data in memory reduces the execution time. Of course this come with a cost. It may not be a big issue with modern web application, but it could be a concern in some special environments like embedded software and, in general, with resources-limited hardware. The rule of thumb is to know in advance if there are such requirements and, in this case, decide carefully whether we can store data in memory and which data. If we have such limitations, we could apply some techniques like:

  • using primitives types and store in memory only the most important values instead of big objects;
  • using paging or similar techniques when retrieving data from the back-end.

Accessibility and security

Looking back the example of Fig. 1, during the build phase the data is taken from different servers within the company network, like a server with the code base (gitlab or github, subversion, etc..), repositories with other internal libraries or third party libraries, etc..

These locations reside inside our network and cannot be reached from outside. This gives the advantage of security, since we can have the raw data (code, configuration, etc.) in our internal servers and we put in the final artifact only a compiled and secured version of them.

Here the rule is that if we have sensitive information that we don’t want to expose or saved in restricted servers inaccessible from outside, we can set up a task at build time to recompile this data into an artifact.

Separation of Responsibility

Having the data distributed in several places follows the principle of separation of responsibility. It will be easier to apply change in one library or configuration file without touching the rest of the code base or the pipeline.

It is generally recommendable, especially for complex logic, to apply this principle and decentralize the configuration of our software.

Conclusions

There are many ways to configure a software and here we focused on few of them: at development time, at build time or at runtime. We, then, took few characteristics and evaluated each option to understand their pros and cons.

In the real world there will be more aspects or requirements that we need to consider and some of those requirements may have a higher priority or may be not negotiable. For example, if I have strict memory requirements I am limited in the amount of data I can load at startup. Other times we are more flexible and can choose more freely how to implement the configuration in our software.

Either way, we have to ask ourselves what we want to accomplish and what our requirements are (in terms of memory, complexity, execution time, security, etc..). From there, we must evaluate each option and choose the one most suited to out needs, understanding the treadoffs that come with it.

--

--

Ricky Iudica
Geek Culture

Wild Software Engineer with the passion for sports and physiology, follows his logic half of the time and his sense of aesthetic the other half!