Compositional Software Design

Jakob Jenkov
Last update: 2024-07-03

Compositional Software Design is the name I have given my personal software design style which focuses on designing your codebase for composability. You design for composability to increase the reusability and testability of your codebase. Designing for composability also tends to reduce the size of your codebase.

You design for composability by decomposing your codebase into smaller units which can be composed together to form composite solutions to a variety of problems. These smaller units, sometimes referred to as components, become the low-level building blocks of your application, service, API or whatever software you are developing. Of course you may need some "glue code" in between the units to write them together to form the final solution.

You only design for composability in the situations where you actually benefit from the composability. In some situations you might not need the reusability of certain pieces of code, so you abstain from decomposing that code. Similarly, in some situations achieving high performance may be more important than achieving reusablity, so you design for performance (e.g. using Data Oriented Programming) rather than for composability in that situation.

Sometimes I may refer to Compositional Software Design simply as Compositional Design because it is shorter to write. I am referring to the same concept, however.

Why Compositional Software Design?

I started looking into what eventually became compositional software design in 2023 - and have since then been working on refining the ideas to be more concrete and more easily applicable. Before diving into the finer details of compositional software design, I want to give you a bit of history of where I got the ideas from.

Reusability is Not a New Idea

Let me first make it clear that in 2023 there was nothing new about breaking your codebase into reusable units. This has been a good practice for as long as I can remember (I started programming in 1987) - and probably long before I remember too.

What is New Then?

What is new in compositional software design is not the idea of reusability or designing for reusability - but the way we design for reusablity.

The focus in compositional software design is on why, when and how we split a unit into multiple units. What the resulting construct "looks like" is less interesting than why we split the code up into that construct. In other words, whether the resulting construct implements Design Pattern A, B or C is not as interesting as why we decided to decompose into that construct in the first place.

But - I am getting a bit ahead of myself here! I will explain all this in more detail throughout the rest of this tutorial.

What Triggered These Ideas?

Compositional software design is my attempt at reverse engineering and distill the design thinking that went into the creation of Design Patterns, Dependency Injection, Object-Oriented Programming, Functional Programming, and other similar design techniques. In other words, my attempt at discovering the underlying fundamental ideas behind these design techniques.

The reason I started looking into this is, that I started seeing a lot of criticism of object-oriented programming, and claims that either functional programming or other design techniques were better. However, my intuition told me that the criticism was missing something.

The criticism often referred to object-oriented programming as described by the classic academic object-oriented-programming theory. However, I do not feel that the classic object-oriented programming theory is an accurate representation of object-oriented programming anymore - and it probably has not been since the mid 2000's. Classic object-oriented programming theory does not mention design patterns or dependency injection. Nor does it mention designing for replaceability or removeability.

The more I looked into the underlying ideas behind object-oriented-programming and its alternatives, the more I found them to attempt to achieve the same goals. I find that object-oriented-programming and functional programming are actually quite similar in many respects, and not two completely different paradigms.

I also found, that strict adherence to design doctrines is not always a good idea. Design doctrines and dogma should be considered to be guidelines rather than hard rules. Deviate from them whenever it makes sense to do so (but be ready to explain why it makes sense to deviate in that specific situation). But this is a ifferent discussion which I am talking more about in my tutorial about Conscious Software Design.

Design Thinking vs. Design Solutions

In general, whenever we have a software problem we apply some kind of design thinking which produces some kind of solution. We often refer to this solution as a "design". This process is illustrated here (a bit simplified):

Design Thinking

There is no problem with this simple process by itself.

The problem is, that we tend tro try to reuse the resulting solutions rather than reusing the design thinking that went into producing the solution. We tend to name and memorize the different solutions - and later to attempt to reapply those same solutions. We think in terms of "legs" and "wheels" rather than "transportability across different surfaces".

The growing catalogue of design patterns is a great example of this. We think in terms of "factories", "abstract factories", "service locators" or "dependency injection" rather than "implementation replaceability".

If we try to apply the same solution in two different situations, we are essentially trying to make our problem fit the solution, rather than making the solution fit our problem.

What I would like to do instead is - not to reuse the design solutions - but to reuse the design thinking that led to those solutions. This should help us produce solutions that are specifically designed for each specific situation.

Put differently, I believe we should focus on design thinking rather than design patterns. This also means focusing on appropriate practices rather than best practices.

Composition as a Common Theme

So - what do design patterns, dependency injection, OOP, FP, SOLID etc. have in common?

Well - the main theme I see run through all of these ideas is composition. Various ways of composing objects or functions together to solve certain problems or achieve certain possibilities.

Thus - I started looking for ways to design for composability. The results are the principles outlined in this article about compositional software design.

Note, that Data Oriented Programming is an outlier here. The underlying principle of Data Oriented Programming is to design your code for efficient access to, and iteration of, larger amounts of data. The goal is performance, and it is achieved by designing your code to align more with how the underlying hardware works. You can combine Data Oriented Programming and Compositional Software Design if you want to - using each style where it makes sense in your codebase, but the styles do not share a common underlying theme.

Composability Increases Reusability and/or Testability

The main benefit of composability is that it increases the potential for reuse. With reuse you get fewer bugs and faster development speed. Sometimes you get a bit more readability too - but that depends on the design of the components. It is certainly possible to design components in a way that makes them less readable.

Another possible benefit of designing for composability is, that your components tend to become easier to unit test. Or - at least you can design them to be easier to test using the techniques described in this tutorial.

Why, When and How to Split Your Code

As mentioned in the beginning, the focus in compositional software design is on why, when and how we split a unit into multiple units.

Why: We typically split a unit of code into multiple units to achieve increased reusability, or increased testability.

When: We don't have to split a unit into multiple units until we will actually achieve increased reuse - or increased testing, meaning some of the code will be actually be reused, or we will actually write a more thorough test for the new units than we already have.

How: Splitting up a unit into multiple units typically follows the following steps:

  1. Identify a responsibility that you want to extract - because we want to be able to reuse, replace, remove or otherwise amend it.

  2. Make the split. Extract the code we have identified for extraction.

  3. Improve reusability of all units resulting from the split - not just the extracted unit. We will see ways to do that later in this tutorial.

The three steps under how to split up a unit into multiple units are correct - but not very specific. They could be more concrete - so it was easier to know what do to in practice.

To alleviate this lack of specificity and concreteness - Compositional Software Design has three reasonably concrete core principles, as explained in the next section.

Compositional Design Principles

Compositional Design has three core design principles:

  • Narrow Responsibility
  • Widen Applicability
  • Reduce Use Effort

Narrowing responsibility means splitting a unit of code into multiple units, each with a narrower responsibility than the unit that was split.

Widen applicability means that you change one (or more) units so they can be used in a wider set of use cases. Widening the applicability of a unit thus increases it reusability. This is typically done by changing its functionality subtly as well as changing its interface.

Reduce use effort means that you reduce the effort required to use one or more units. You make the units easier to use, in other words. This is typically done by modifying its interface (e.g. via overloaded methods) or by adding extra supporting components (such as factories or builders etc.).

Static, Dynamic and Polymorphic Composability

There are three levels of composability that you can design for in your code:

  1. Static Composability
  2. Dynamic Composability
  3. Polymorphic Composability

I will explain the difference between each of these composability levels in the following sections.

Static Composability

Static composability means that a function A that uses another function B - does so by referencing B from within the code of A in a static fashion. In other words, the reference to B cannot be changed at runtime. It is static (hardcoded). Here is an example of static composability:

public void a() {
   String result = b();
}

private String b() {
    return "" + System.currentTimeMillis();
}

The call to b() from inside of a() cannot be changed at runtime. It is static. Thus, the composition of a() and b() is static.

Dynamic Composability

Dynamic composability means that components are designed so their composition is decided at runtime. This is typically done via dependency injection - via constructor injection. However, once a dynamic composition has been made, it cannot be recomposed. It can be dynamically composed, but not re-composed at runtime. Here is an example of dynamic composability:

public class ComponentA {

    private ComponentB componentB = null;

    public void ComponentA(ComponentB componentB) {
        this.componentB = componentB;
    }

    public void a() {
        this.componentB.b();
    }
}
public class ComponentB{

    public String b() {
        return "" + System.currentTimeMillis();
    }
}
ComponentB componentB =
    new ComponentB();

ComponentA componentA =
    new ComponentA(componentB);

componentA.a();

Notice how it is possible to compose a ComponentA and a ComponentB together at runtime, but once they are wired together, you cannot re-compose them.

Polymorphic Composability

Polymorphic composability means that components are designed to be both composed and re-composed at runtime. This is typically achieved using dependency injection via setter injection, or by passing a dependency as a parameter to a method or function - using call-time composition.

Here is first an example of polymorphic composability via setter injection.

public class ComponentA {

    private ComponentB componentB = null;

    public void ComponentA() {}

    public void setComponentB(ComponentB componentB) {
        this.componentB = componentB;
    }

    public void a() {
        this.componentB.b();
    }
}

Notice how the constructor injection from the example in the dynamic composability section has simply been replaced with setter injection. Everything else is the same. However, now a ComponentA instance could have it's ComponentB instance replaced at runtime.

Here is an example of polymorphic composability via call-time composition - meaning where the dependency (ComponentB) is passed in as parameter to the method that uses it:

public class ComponentA {

    public void ComponentA() {}

    public void a(ComponentB componentB) {
        componentB.b();
    }
}

Using this design you can re-compose ComponentA with an instance of ComponentB every time you call the a() method. This gives flexibility, but also makes the code a bit more verbose, as you will have to declare the composition every single time you call a() . If you need to call a() in more than place in your code with the same ComponentB instance, this will make your code a bit more repetitive to look at.

Jakob Jenkov

Featured Videos

Java Generics

Java ForkJoinPool

P2P Networks Introduction



















Close TOC
All Tutorial Trails
All Trails
Table of contents (TOC) for this tutorial trail
Trail TOC
Table of contents (TOC) for this tutorial
Page TOC
Previous tutorial in this tutorial trail
Previous
Next tutorial in this tutorial trail
Next