DEV Community

Cover image for Programming Through Composition
MXI.Studio
MXI.Studio

Posted on Originally published at mxi.studio AI-assisted

Programming Through Composition

> There is a point where a framework stops simplifying the project and the project starts reorganizing itself around the framework. That is usually when the abstraction has become more expensive than the problem it was meant to solve.

I’ve built enough software at this point to notice a pattern. A lot of the time, I don’t actually need a huge framework. I need a few things that work well.

A parser. A networking layer. A renderer. Some math. A bit of concurrency. Maybe a native layer where performance matters. That’s usually it. And those are usually readily available in forms that are optimized, lightweight and sometimes even cross platforms by default.

The problem starts when I go looking for a “complete solution” and end up pulling in something that solves far more than I actually need or just solves a lot of things I don’t need. Then the project starts growing around the framework.

Now I need the framework’s lifecycle. Its abstractions. Its configuration system. Its plugin model. Its dependency tree. And when I finally hit something the framework does not fit cleanly, I add a wrapper.

Then another library. Then some glue code. Then a workaround. Somewhere along the way, I’m no longer solving the original problem.

I’m solving the framework against my needs in order to be able to use it to solve my initial problem.


It changed my entire approach to building systems. Start with capabilities, not with frameworks. The question I now ask myself is straightforward; What does this project actually need to do? Not; what framework should I use? But rather what capabilities are required? This difference is tiny, but the result is monumental.

If I need JSON parsing, I can use a mature JSON library. If I need networking, I can use a focused networking library. If I need linear algebra, I can use a math package that already does it well. If I need rendering, I can pick a renderer that does not also try to own my application.

The goal is not to avoid dependencies. It is to choose dependencies that have a clear job. Stitch the pieces together yourself. Once the core capabilities are there, I build my application around them.


I would rather write a relatively small amount of glue code that I fully understand than adopt a large abstraction that forces the project into a shape I did not choose. In doing so, I gain something that is very important to me: architectural ownership.

I know what each component does. I know why it is there. I know where the boundaries are. And if something breaks, I have a much better idea of where to look. That makes debugging, refactoring, and optimization much easier. It makes sliding in error management a lot easier.

A very complex tool can cover multiple needs, but it does not always replace the efficiency of a well-composed toolbox.

But don’t rewrite everything either. This is not a “build everything from scratch” argument. That usually makes no sense. If a mature library solves the problem well, use it. And there is a lot of powerful libraries out there waiting to be used.

The interesting part is knowing when the abstraction has become larger than the problem. Sometimes using a giant generalized system means spending more time adapting it than it would take to build the exact behavior needed from a few smaller pieces. It also means taking the risk to not be able to fix issues, if these issues are unrelated to your code.

That is usually the point where I reconsider the architecture.

We need to ask ourselves if we are saving time by using this tool, or are we now maintaining a translation layer between the tool and my project? If the answer is the second one, composition often starts looking a lot better.


Separate contexts when it helps

Another thing I stopped assuming is that the whole application needs to live in one environment. Some parts are better low-level. Performance-sensitive logic, simulation, rendering, memory-heavy work, networking internals.
Other parts are better high-level. Scripting, orchestration, experimentation, interfaces, automation. So, a perfectly reasonable setup might be:

  • C++ for the performance-critical core
  • Python for orchestration
  • bindings between the two

The binding layer is not a trick. It is an architectural element. You get control where we need it and speed of development where iteration matters.


AI makes this easier than it used to be

This is also the area in which I have found AI to be of genuine use to me. Not in making decisions about the architecture. That still needs my judgment. But AI is superbly well suited for minimizing the annoying stuff that surrounds composition. Understanding an unfamiliar API. Writing a binding. Explaining a dependency. Generating boilerplate. Finding the right integration point. Translating documentation into something immediately useful.

Those tasks used to add enough friction that reaching for one large ecosystem was often easier. That trade-off is changing. It is becoming much more practical to assemble a system from focused, mature components and still move quickly.

At this point, I prefer to start small. Find the exact capabilities the system needs. Pick proven components for those capabilities. Build the project-specific layer myself. If needed rebuild some layers and own them. Keep the boundaries visible. Use multiple languages when they actually help. And avoid adopting complexity just because it comes bundled with something popular.

That is what I mean by programming through composition.

Not fewer tools. Better-chosen tools. Not less complexity at all costs. Complexity that I can actually see, understand, and control. That is the version I keep coming back to now.

Top comments (0)