AI Code Won’t Be for Humans Much Longer (AI impressions, part 2 of 5)

This is the second part of the series “Impressions of Our Current AI Usage”, as outlined by the introduction article.

In the first years of software development, the word “source code” didn’t exist, because code was just that: encoded machine instructions. How they were encoded changed rapidely, from flipping bits in the RAM directly by mechanical switches over feeding paper tapes with punched holes to magnetic storage. But for a long time, we worked with none or little abstraction over the actual machine code. I remember assembler code listings that had two columns of text: the first column for the computer, the second one just translating the first column into human-readable text.
And even with this little bit of clarification what the code actually does, we already needed additional software that took our source code and translated it for the machine.
With the adoption of higher-level programming languages, the additional software stack grew in depth until the distance between the source code and the actual machine instructions was big enough to warrant an intermediate layer of representation. Programming languages like Java or C# put a “byte-code layer” between our textual source code and the binary machine code. The machine we program against is no longer a real computer, but a “virtual machine” or in better words, a model of a machine. As long as we write source code that works correct with the model, we can assume that all the translation layers will find a way to run it correctly on the real computing substrate.
We are used to this kind of programming. We describe our goals using the machine model and a sophisticated machinery of software and hardware parts make it happen.

Forward to today and we use artificial intelligence (or inference using another kind of “model”) to produce source code in our favorite programming languages by describing our goals in even broader terms than before. We might mistake our prompts for natural language and think that we are able to produce source code by just saying what we want.

The question that poses itself nearly instantly is: If we invented a device that transforms natural language into machine behaviour from scratch today, would we include all the intermediate layers into its inner works? Is it really a good idea to transform natural language into higher-level source code, compile the source code into byte code and do all the weird magic to come up with a sequence of machine instructions that resemble the byte code? Isn’t it more efficient to teach the inference how the actual machine works and let it program directly?
Or, asking from the other side, who is the target audience of the generated source code if nobody reads it and the compiler only parses it once? Why does the inference invent all the variable and method names when the compiler throws them away again in the first step of its processing? Of course, right now the inference only imitates our way of working. But we work like we do because we write source code for other humans. If the human at the helm can’t read any layer of code anyway, why not jump directly to the most obscure representation of code and skip all the readability requirements?

As soon as the inference doesn’t imitate but actually learns about the target computing substrate, it will produce working code that is undecipherable for human readers, but optimal for the machine. (If you want to experience this effect in a tiny dose, I encourage you to play the “TIS-100” programming game). And because most inference users don’t need the readable code anyway, they won’t miss anything and get faster solutions with less hassle.

So my guess is that today’s source code will be a dying art, invented for humans and ignored by the machines because it doesn’t provide anything useful for them. The source code of the future will be less readable, more enigmatic and probably more efficient for the machine. Which means that human intervention or even just participation in the software production process will become more cumbersome and therefore even more limited.

My sorrow is that this distancing of the programming process from actual human oversight might provide a hard depedency on inference work alone. It would mean that humans aren’t just scales slower than the inference, but actually incapable of doing its work by hand anymore.

Quotes Are Not Part of the Argument

Recently, we upgraded a project from an older Java version to a newer one. Many of the changes were routine: update dependencies, replace deprecated APIs, fix a few compiler errors and run the test suite.

One of these changes concerned the invocation of an external Windows program.

The application used the deprecated overload:

Runtime.getRuntime().exec(command);

The command was assembled as one long string. The external program accepted command-line parameters of the following form:

/a="value of A" /b="value of B" /c="value of C"

Because the parameter values could contain spaces, we enclosed them in double quotes. We even had unit tests that explicitly verified the quoting. It was an important detail, or so we thought.

As part of the upgrade, we switched to the recommended overload that accepts the executable and its arguments separately:

Runtime.getRuntime().exec(commandArray);

The migration seemed straightforward. Instead of joining the executable and all parameters into one command string, we put them into a string array:

String[] command = {
"program",
"/a=\"value of A\"",
"/b=\"value of B\"",
"/c=\"value of C\""
};
Runtime.getRuntime().exec(command);

The unit tests were still green. The quotes were still present. Everything looked correct.

Then the application was deployed.

Invalid switch

In production, the external program stopped accepting our invocation. Its only diagnostic message was:

Invalid switch

This was not particularly helpful.

We inspected the parameters. All switches were present. Their spelling was correct. Their order was correct. The values were correct. The quote characters were exactly where our tests expected them to be.

Even more confusingly, the command worked perfectly when entered manually in a Windows command prompt:

program /a="value of A" /b="value of B" /c="value of C"

The executable clearly supported these parameters. The shell command clearly worked. And our Java code appeared to produce the same command.

But it did not.

Asking the receiving program

After spending some time comparing strings and staring at quote characters, we decided to stop reasoning about what the external program ought to receive. Instead, we wrote a small program that showed us what it actually received:

void main(String[] args) {
for (var i = 0; i < args.length; i++) {
System.out.println("args[" + i + "]: '" + args[i] + "'");
}
}

We packaged it as a JAR and invoked it from cmd.exe using the same parameter structure:

java -jar exec-experiment.jar /a="value of A" /b="value of B" /c="value of C"

The output was:

args[0]: '/a=value of A'
args[1]: '/b=value of B'
args[2]: '/c=value of C'

The double quotes were gone.

This was the missing piece.

The quotes in a shell command are not necessarily characters intended for the receiving program. They are instructions to the command-line parser. They tell it that a sequence containing spaces belongs to one argument.

The value

/a="value of A"

does not mean that the program receives an argument containing two quote characters. It means that the program receives one argument rather than three:

/a=value of A

The quote characters control parsing. They are not part of the resulting argument.

Quotes can appear in surprising places

To verify this interpretation, we performed a slightly more unusual experiment:

java -jar exec-experiment.jar /"a="va"lue of A" /b="value of B" /c="value of C"

This command is certainly not how anybody would normally write the parameters. Nevertheless, its output was unchanged:

args[0]: '/a=value of A'
args[1]: '/b=value of B'
args[2]: '/c=value of C'

The quotes can surround different portions of a token. Their purpose is to influence how the command line is divided into arguments. Once parsing is complete, they disappear.

This distinction is easy to overlook because a command line is usually presented as a string. It looks as though this string is passed to the program. In reality, there are two different representations involved:

program /a="value of A"

is a textual command line that still needs to be parsed.

By contrast,

new String[] {
"program",
"/a=value of A"
}

already describes the result of that parsing: an executable followed by one complete argument.

We had moved the parsing boundary

With Runtime.exec(String[]), every array element already represents one argument. Spaces inside an element do not split it into additional arguments.

By retaining the quotes, we had changed their meaning. They were no longer syntax interpreted by a shell-like parser. They had become literal characters inside the argument:

/a="value of A"

That was not what the external program expected. It expected:

/a=value of A

The error message “Invalid switch” was therefore accurate, but not very illuminating. The switch looked correct in our logs because we were looking at its command-line representation rather than at the argument format expected by the program.

The fix was simple:

String[] command = {
"program",
"/a=value of A",
"/b=value of B",
"/c=value of C"
};
Runtime.getRuntime().exec(command);

Or, preferably, using ProcessBuilder:

Process process = new ProcessBuilder(
"program",
"/a=value of A",
"/b=value of B",
"/c=value of C"
).start();

After removing the quote characters, the external program worked again.

Ironically, the quotes that our old implementation and its tests had treated as essential were exactly what broke the new implementation.

The takeaway

A command line and an argument array are not interchangeable representations.

When constructing a command line, quoting may be required to preserve spaces during parsing. When constructing an argument array, parsing has already happened conceptually. Each element is one argument, spaces included.

Do not ask:

How would I type this command in a shell?

Ask:

What exact strings should the receiving program find in its argument array?

The answer to the second question is what belongs in Runtime.exec(String[]) or ProcessBuilder.

Sometimes an API migration changes more than a method signature. It moves a boundary – in this case, the boundary between formatting a command line and supplying already separated arguments.

And when that boundary moves, yesterday’s carefully tested solution can become today’s bug.

Great software engineers are transformers

Created by me; using https://www.deviantart.com/dreamup

…or transformators. Often when I meet people and talk to them about their jobs or everyday life topics come up about suboptimal processes and workflows and other complexeties we have to deal with. Almost everytime such issues arise my brain start a background process working on ways to improve the talked-about situation.

Many of my developer colleagues and friends are the same: We all shake our heads or face palm and immediately think about possible remedies.

Talking to non-developers about the same issues often leads to reactions like

  • It cannot be changed
  • It has been this way since forever
  • We have never done it that way

None of the great (software) engineers I had the pleasure to work with thinks this way:

They all try to understand the context, domain and status quo. As a part of this process often the first problems are uncovered. In addition we define aims and target metrics together with the domain experts who often are part of one of several user groups.

On that basis they develop solutions fitting to the situation at hand. All the constraints in time, resources and knowledge are taken into account. Knowing that the initial scope usually does not cover a full solution, an evolvable system is designed that already provides value. Over time they add features, fix blind spots and weaknesses and gradually expand the scope.

This analytic view and the incremental process towards a system that improves the current situation is key in pushing things forward. It also eliminates most of the “impossible to implement/change” counterargument.

The latter two main arguments against change revolve often about excluding user groups like elderly people or people accustomed to the status quo unwilling to adapt. They can be mitigated by designing the systems and services to have multi-modal inputs and outputs. Working with them can stay largely unchanged for inert users groups while others may utilize the new options the solutions offer.

Recently, I heard of a nice example of this: Customer banking already is largely digitalized but there are banks that still offer inboxes for credit transfer on paper in addition to online-banking and apps. People without digital devices or knowledge simply throw the papers into the inbox where it automatically gets scanned and digitally processed. And there is still the option to snail-mail the account statements for the people who do their management on paper. All the process in between is automated and digital leaving no one behind.

In https://schneide.blog/2026/01/19/digitalization-is-hard-especially-in-germany/ I described general guidelines to make mostly analog real-world processes digital and frictionless.

When Optional sounds too optional: opt for more expressive types

So, I have one PyQt application which not only is quite data-heavy, but also has significant real-time requirements, as well as multiple windows. This construct brings some absolutely horrifying highly intellectually inspiring quests with it, and Python turned out to be kind of a good decision for that project, because that is one of the languages where, when you think about your structure a bit, you might get to write very natural-sounding like code.

Of course, the following idea is actually language-agnostic, I will just use fictive Python examples close to problems-based-on-a-true-story.

This in itself is not only a matter of aesthetics, but because real-time demands are quite tricky to reliably be covered by unit tests alone, the actual code has to read itself so clearly that one does not need to second-guess what any of this does. Think of a bedtime story, which usually would not, coming to think of it, contain clauses – or paragraphs, for that matter – requiring, under circumstances not even trivial to the human eye, one kind of meticulous gymnastics, easily negating twice, or thrice, and relying on Python’s borderline criminal degrees of freedom in duck typing, or canards even– you see — your toddler will now not go to sleep anytime soon. Or trust you with another story, for that matter.

Now I found out: While Qt is somewhat mature, one cannot even trust their way of doing things – i.e. turns out, the signals/slots system is not particularly designed for performance. Neither did I feel inclined to put my faith into even another state management solution like e.g. python-statemachine package, because – as capable as that sounds, it might be overkill, and distracting with its own idiosyncrasies (as also: I would not recommend Redux for a web project anymore, especially in TypeScript, except for you really know from the start that this is a good fit).

But, so, I have some tricky interplays between

  • Data consistency / single-source-ness demands that e.g. between two windows, there should only be primitive data exchanged, say str/int identifiers, and both have access to their repositories; not throwing loaded data sets around my memory in order to go stale at times
  • Comprehension, most significantly Single Level of Abstraction, or other indicators of mental load like how many levels of intendation / return paths are mixed within sight (and also, Type Annotations do help a lot in Python, even though not mandatory, i.e. the complete opposite of fighting Redux-TypeScript-chimaeras – but I digress…
  • Robustness, where I would believe that my user (me) has virtually no chance of even seeing this and that window when their data is maybe still loading somewhere – but I still check these cases, because this bedtime story has no business in leaving you an hopeful-to-anxious pile of nerves
  • Traceability of your state, for troubleshooting and useful UI feedback (as you’d guess, real-time event based stuff is not easily debugged by break points or logging alone).

So over months in that project, I grew annoyed of code like (Symbolbild)


class Editor:
    # ...
  
    def load_editor(self, params: Optional[EditorParams]):
        if params and (self._entity is not None or
                       self._entity.id != params.id):
            if entity := repository.load_entity(params.id):
                self._entity = entity
            else:
                raise ValueError("repository needs some alone time :(")
            self._entity.other_stuff = other_repo.check_stuff()
        elif params is None:
            raise TypeError(
                "sounds Optional in our signature, but actually is not"
            )
        elif self._entity.id == params.
            self.adjust_more_stuff(self._entity, params.stuff)
            # ...

Because encountering any single block of these drags you down, I have currently accustomed myself to write these as (one can argue whether the names like “Supplier” are the best here, but they’re not the worst, I believe)

@dataclass(frozen=True)
class LoadedEntity:
id: str
entity: Optional[Entity]
stuff: Optional[OtherStuff]
@property
def is_unusable(self):
return self.entity is None
@property
def missing_stuff(self):
if self.is_unusuable:
return True
else:
return self.stuff is None
class EntitySupplier:
_current: LoadedEntity
_entity_repo: EntityRepository
_stuff_repo: OtherStuffRepo
# __init__ etc. hereby left out as boilerplate
def load_params(self, params: Any):
# do all your checks in here
if (... very bad ...):
self._current = LoadedEntity(params.id, None)
return
entity = self._entity_repo.get(params.id)
stuff = self._stuff_repo.get(params.stuff).for(entity)
return LoadedEntity(
params.id,
entity,
stuff
)
@property
def entity(self):
return self._current.entity
def expecting(self, stuff: bool = False) -> Optional[LoadedEntity]:
if stuff and self._current.missing_stuff:
return None
return self._current
class Editor:
_supply: EntitySupplier
_logger: SomeLogger
def __init__(self, **kwargs):
self._supply = EntitySupplier(**kwargs)
self._logger = BlaBlaLogger()
def load(self, params):
self._supply.load_params(params)
if entity := self._supply.entity:
self.update_ui(entity)
else:
self._logger.error("Outsmarted, eh? %s | %s", str(params), stack_trace())
return
if supply := self._supply.expecting(stuff=True):
self.initiate_stuff_from(supply)
else:
self._logger.info("Entity %s is ready, Stuff is not | %s", str(entity), stack_trace())

So, the LoadedEntity serves like a concatenation of several Optional types, but it wraps the logic (i.e. there’s no sense in having stuff when you don’t have entity first) instead of just shruggingly claming “well, this entity here is optional – and that other stuff is, too”. Now, LoadedEntity is not a pretty name at all (have a better one?), but it sure beats having two straightaway lies.

I like that pattern because it allows me to stash the EntitySupplier and LoadedEntity somewhere on their own (I do strictly not believe that every class needs its own file, but some of the “Single …” ideas (Responsibility, Level of Abstraction, you name it) do also apply here; and the Editor.load(…) itself does read somewhat like a short story. It has quite linear structure and can early-return, and/or log, on demand, and while naming is still hard (consistently voted one half of famous Hard Things), I could even have some fun in designing that language while preserving the idea, that future-me can arrive in a few weeks (read: hours) and still trust in some of the entites and stuff.

The quintessence here is: Checking for None (which is Python’s NULL, and the typing Optional[T] is identically equal to T | None) is still a thing in 2026 due to its sheer practicality, but if you design some some structure around that and keep these checks in something like LoadedEntity, you can keep the abyss from staring back into you.

Rails Strict Locals: Giving Partials an Explicit Interface

Rails partials are a great way to reuse view code, but they have traditionally suffered from one weakness: their interface is implicit.

When opening a partial written by another developer, it is often unclear which locals are required, which are optional, and whether all of them are still used. IDEs typically cannot help much either, often showing warnings about unresolved variables because they cannot determine where the values come from.

The problem becomes even more apparent as an application grows and partials are rendered from multiple places.

If a local is forgotten by call, the error only appears when the template is rendered:

undefined local variable or method `missing_local'

If an extra local is passed, Rails traditionally ignores it.

Over time this creates a situation where the real API of the partial exists only in the heads of the developers maintaining it.

Rails Strict Locals

Rails provides a feature called strict locals that allows a partial to declare its expected interface:

<%# locals: (title:, highlight: false) %>

The declaration resembles Ruby keyword arguments and is placed at the top of the template.

A local like title without a default value is required. Locals like highlight with default values become optional

The partial now documents and enforce its own API. If a required local is missing, Rails raises an exception instead of failing later when the variable is accessed. Likewise, if a caller provides a local that is not declared, Rails rejects it.

Conclusion

Strict locals do not fundamentally change how partials work, but they make them easier to understand and maintain.

By declaring the expected locals directly in the template, partials become self-documenting and gain an explicit contract with their callers. Missing locals are detected early, obsolete locals are rejected, and developers no longer have to search through controllers, parent templates, and render calls to understand where variables come from.

An additional benefit is improved tooling support. Once the interface of a partial is explicit, IDEs can understand the available variables much better. Your IDE becomes a helpful companion again rather than a source of noise.

A doubly linked list for entt components

I recently implemented a small CRTP template to group entities in entt. It turned out quite nicely, so let me share it here:

template <class T> class doubly_linked_component
{
public:
using node_type = doubly_linked_component<T>;
static void on_construct(entt::registry& entities,
const entt::entity e)
{
auto& that = entities.get<T>(e);
that.next_ = that.prev_ = e;
}
static void on_destroy(entt::registry& entities,
const entt::entity e)
{
auto& that = entities.get<T>(e);
// List has only this element?
if (that.next_ == e)
{
return;
}
auto next = that.next_;
auto prev = that.prev_;
auto& next_node = static_cast<node_type&>(entities.get<T>(next));
auto& prev_node = static_cast<node_type&>(entities.get<T>(prev));
prev_node.next_ = next;
next_node.prev_ = prev;
}
static void merge(entt::registry& entities,
entt::entity lhs, entt::entity rhs)
{
auto& lhs_node = static_cast<node_type&>(entities.get_or_emplace<T>(lhs));
auto& rhs_node = static_cast<node_type&>(entities.get_or_emplace<T>(rhs));
// The end of left (which is left.prev_) needs to point to right
auto lhs_end = lhs_node.prev_;
auto rhs_end = rhs_node.prev_;
auto& lhs_end_node = static_cast<node_type&>(entities.get<T>(lhs_end));
auto& rhs_end_node = static_cast<node_type&>(entities.get<T>(rhs_end));
lhs_end_node.next_ = rhs;
rhs_node.prev_ = lhs_end;
rhs_end_node.next_ = lhs;
lhs_node.prev_ = rhs_end;
}
static std::generator<entt::entity> enumerate(entt::registry& entities,
entt::entity e)
{
// By default, entities are their own lists
if (!entities.any_of<T>(e))
{
co_yield e;
}
else
{
auto current = e;
while (true)
{
co_yield current;
current = entities.get<T>(current).next_;
if (current == e)
break;
}
}
}
private:
entt::entity prev_ = entt::null;
entt::entity next_ = entt::null;
};

You can use it like this:

struct cool_group : doubly_linked_component<cool_group> {};

By default, each entity represents its own 1-sized group. To merge two groups:

cool_group::merge(entities, left, right);

To iterate over all entities, I am using the new std::generator and coroutines. You can use it like this:

for (auto entity : cool_group::enumerate(entities, head))
do_something(entity);

entt will automatically call into on_construct and on_destroy.

People nowadays usually avoid linked lists because of all the pointer chasing required to actually use them. The pointer chasing is not the problem though, the non-locality is. If you make sure your nodes are all allocated in memory close to each other, there is hardly a penalty. entt will usually do this if nodes are also allocated close in time, which is often the case if you want to group things, so this works nicely in that regard, too. Feel free to use this code under CC0.

Avoiding Code Style Discussions

Every developer has personal formatting preferences.
Brace placement, line wrapping, imports, tabs vs. spaces — everybody has an opinion, and most of them are reasonable.

The problem starts when all these styles meet in one repository.

The cost of “personal style”

A codebase written by ten developers can easily look like ten different applications stitched together. Suddenly, pull requests are full of formatting changes. Git diffs become noisy. Merge conflicts appear because one developer reformatted a file differently than another. Code reviews drift into discussions about whitespaces instead of actual functionality.

Even worse: inconsistent code slows down reading.

Humans recognize patterns quickly. When code follows the same visual structure everywhere, the brain spends less effort parsing syntax and more effort understanding intent.

Consistent formatting reduces cognitive load.

A shared style is less about aesthetics and more about reducing friction. But how to solve this problem?

Shared Project Style

In IDEs like IntelliJ, you can define a code style and automatically reformat code according to those rules. This helps you keep your own code consistent. However, if every developer uses a different style, it does not help the project as a whole.

You can configure the style under:

Settings -> Editor -> Code Style

and save it as a project-level configuration. IntelliJ will then create a codeStyles folder with XML files inside the .idea directory.

The solution for sharing one configuration across the whole project is to commit these files to Git. This way, every developer working on the project uses the same code style configuration.

The IDE can then help enforce the agreed style by reformatting code before commit or even automatically on save.


Consistency beats preference

The important thing is not finding the perfect style. The important thing is agreeing on one.

A consistent codebase is easier to read, easier to review, and easier to maintain. Pull requests become smaller and cleaner because they contain actual changes instead of formatting noise.

Good formatting should be boring and automatic. That leaves more time for discussions that actually matter.

Consistent Structure Considered Harmful

Debating about pros and cons of different code styles quickly tend to enter “which color is best”-territory, so this is not what I’ll do here, but consider the following an internal debate of mine that occurs from time to time.

The “Structure” of a piece of code spans various topics: from syntactical preferences like conventions for curly braces, to case conventions, naming variables, indentation, line breaks and whitespace in general, how to distribute methods between classes, between files, up to its high-level architecture. I am strictly not talking about the higher levels here.

However, in the finer levels of your code, you will apply a mixture of conventional choices coming from the programming language, the culture surrounding it, and your own background. Some will be shaped by your IDE. Keep in mind that all choices are to be done to aim for one goal:

To produce readable code which is straightforward to argue about.

“Readable” is a heavy word as it bears the inseparability of both (a) prerequisites the reader has to fulfill (who are they and why are they even reading my code?) and (b) that getting used to one convention can greatly affect the reading speed more than any in-grained perks of that convention, but nevertheless, there does exist a dimension outside that.

There is some remaining variability in choice, for example, in how to continue the intendation in a multi-line argument list, how to place newlines in chained calls like fluent interfaces / LINQ in .NET / Promises in JS / … or in chained conditionals – the list goes on.

But for example; what is the optimum, e.g. in this Python example

if (self.evaluate_user_input()
and user_role is in (UserRole.Admin, UserRole.ProjectOwner)):
do_stuff()
# vs
if (self.evaluate_user_input() and
user_role is in (UserRole.Admin, UserRole.ProjectOwner)
):
do_stuff()
# vs
if (self.evaluate_user_input()
and user_role is in (UserRole.Admin, UserRole.ProjectOwner)):
do_stuff()
# vs
if (self.evaluate_user_input()
and user_role is in (
UserRole.Admin,
UserRole.ProjectOwner
)):
do_stuff()
# vs ... outsourcing any of that logic into its own place,
# but even that comes with risks of cluttering structure elsewhere.

Another example are braces in any language that uses them, because I’ve encountered a couple of scenarios where the convention would suggest…

if (theThing) {
quiteALot();
ofDifferent();
lines();
} else {
nowSomeCompletely();
differentStuffToDo();
}
# breaking the brace convention, just for that "else",
# conveys a lot purpose to distinguish these branches. for me.
if (theThing) {
quiteALot();
ofDifferent();
lines();
}
else {
nowSomeCompletely();
differentStuffToDo();
}

Line breaks and continuation can be crucial because they influence how far the eyes have to extend to the right (maybe requiring horizontal scrolling, which is a dealbreaker in any measure of quickly-understanding), but whitespace can be beneficial in distinguishing your product from a pile of unicode vomit (which is why gofmt is wrong in believing that inline formulae are always better with any spaces distilled out).

The more I think about it, the less I would agree with anyone convinced that one should just strife towards one certain style and then stick to it. Moreover, the actual content of a line of code can dominate the decision at hand more than a well-meaning thought of “all of these decisions are equal, do not waste any time about them”. The point is, that time saved in reading this can outweigh your time saved in not caring about said reader.

Of course, the title of this post was chosen somewhat demandingly because the actual goal with that is consistent code. The point in such situations being, that uniform guide lines do not automatically lead to consistent expression of intention.

Aim for what your specific piece of code needs to convey, then allow the idea of choosing a style that is not the same choice as for difference pieces of code. Do not overthink it either, but do not think that deviances in uniformity are a code smell.

A more variable, more purposeful coding styles is likely to to conflict when more than one developer is involved (because of the accustomization effect), but treat it like any performance optimization – discuss it when your Merge Review is actually troublesome, with intention, not miles ahead for some hypothetical horror scenario.

Caching Conan Dependencies in Docker for Faster Builds

One problem I often have when dockerizing my C++ Jenkins CI projects is handling incremental builds, for both our own code and the dependencies. Starting builds from scratch can take tens of minutes, too long for my taste.

My build stack is usually conan as a dependency manager and CMake/Ninja for building. Conan will usually try to download precompiled dependencies, but often enough, those are not available for my specific combination of compiler settings and flags, so it’ll build them on demand with the --build=missing flag. That usually takes the bulk of the time needed for a full build. So it makes sense to keep the dependencies cached, once they are built. However, since we use Docker to setup the build environment, they are all lost by default.

Who Owns What?

The obvious solution is to mount a folder on the build host to keep the conan cache using the -v / –volume option for docker run. This can be done by setting the CONAN_HOME environment variable, and I usually use one cache per build folder, which seems like a good compromise between speed and isolation.

But that causes other problems: docker will create all the files for the user inside the container, which is root by default, creating a whole bunch of files that the CI host user cannot delete, e.g. when a branch gets deleted. This breaks the CI setup to a point where manual intervention is required. A somewhat simple clutch is the -u user:group option to docker run, which will execute the build with the given user. The problem I was having with that, however, was that this user did not have access to user-scoped tool installations like conan via pipx.

User-specific Images

My current strategy to deal with this is to inject the host CI user and group into the docker ‘builder’ image, and then do all the building in the container using that user, as if using the CI host user on the metal. The Dockerfile looks like this:

FROM gcc:14.1-bookworm
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get -y dist-upgrade
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get -y install \
cmake \
debhelper \
ninja-build \
python-is-python3 \
python3-pip \
pipx
ARG HOST_USER_ID
ARG HOST_GROUP_ID
RUN groupadd -g ${HOST_GROUP_ID} hostgroup && \
useradd hostuser -u ${HOST_USER_ID} -g ${HOST_GROUP_ID} -m -s /bin/bash && \
mkdir /conan_home && chown hostuser:hostgroup /conan_home
USER hostuser
ENV PATH="$PATH:/home/hostuser/.local/bin"
RUN pipx install conan
ENV CONAN_HOME=/conan_home
WORKDIR /build_root
# Build the viewer deb package
CMD ["/source_root/build.sh"]

After doing the user-independent setup, this declares two ARGs for retrieving the user and group IDs, and then sets up a user with those in the docker image, calling it hostuser:hostgroup internally. Note that the names will not leak out of the container, only the IDs do.

It installs conan via pipx as that user and makes sure it is in the PATH for the build later. This is the real advantage of passing the user into the image creation: user specific things can be installed!

In our Jenkinsfile, I build the image from that while injecting the current user via the –build-arg option:

docker build . --iidfile docker_image_id \
--build-arg HOST_USER_ID=`id -u` \
--build-arg HOST_GROUP_ID=`id -g`

This expects three folders to be mounted: /source_root for the sources/repository, /build_root for the out-of-source build, and /conan_home for the conan cache. Important: make sure these folders are created by the CI user before passing them to docker, or it will create them with the wrong owner. I’m only creating the latter two, since the first one is obviously created by Jenkins.

mkdir -p docker/build docker/conan

Once the folders are set up and the image is built, I run the actual build in a container via:

docker run --rm \
-v `pwd`:/source_root:ro \
-v `pwd`/docker/conan:/conan_home \
-v `pwd`/docker/build:/build_root \
`cat docker_image_id`

That should run the actual build and populate the conan cache. After that I extract the artifacts I need and remove the docker image and ID file with:

docker image rm `cat docker_image_id` && rm docker_image_id

And we’re done!

The future of Grails

Many long-term readers of our blog may have noticed a post about Grails Framework topics every now and then. We are using Grails for more than 15 year both in customer projects and internal ones.

Sometimes using the framework was fun, productive and bliss. At other times it could be frustrating upgrading, chasing bugs or finding workarounds. Occasionally, performance could be a problem. Most of the time it was a solid framework with solid output and customer value.

The more recent past

Ownership/stewardship of Grails changed several times over the years from one company to another. Updates were very infrequent, the general direction was very unclear and the future of the framework extremely uncertain.

Because all of the above we did not start new projects using Grails and did not recommend it to potential customers but instead used other frameworks like Micronaut, Javalin or .NET.

But suddenly there was light at the end of the tunnel: Grails was handed over to the Apache Software Foundation (ASF) in the middle of 2025 and became a top-level project there. That by itself may not be a complete turnaround and rescue for the framework, but it certainly sparked a bit of hope into the whole situation.

The presence and future

Since the adoption of Grails by the ASF a lot has changed. Tons of work has been going on in the background to streamline future development, work on reproducible builds (as required by the ASF) and to enable frequent releases, updates and improvements.

Grails 7 was born and release under ASF stewardship. The community is more open than ever and it seems to be growing again after years of stagnation or even decline.

The roadmap and plans for the next months is clear and the project is moving with a steady pace towards the goals. Of course, there is a lot of work to do, like reviving and porting several plugins to enable all users to migrate to Grails 7 and beyond – but it is happening.

Conclusion

If all the positive changes under the stewardship of the ASF continue, the future of Grails can be bright for the years to come. At least, it does not pose a liability or risk for its users and their customers anymore.

In my opinion, two things besides all the technical stuff are most important:

  • The strong commitment of the ASF to further develop and enhance the project provides safety for developers and customers investments
  • The mindshift from an exotic framework to a productive, JVM-based framework leveraging standard technologies from the java/spring/hibernate ecosystem provides familiarity and stability

Imho, this quote of the Project Management Committee (PMC) chairman James Fredley says it best:

Grails is NOT an exotic outlier. It’s PRODUCTIVITY LAYERS on top of SPRING BOOT.