Compositional Software Design

Jakob Jenkov
Last update: 2024-05-09

Compositional Software Design is a design technique which focuses on designing your codebase for composability. 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.

I first got the ideas for compositional software design in 2023 - and have since then been working on refining the ideas to be more concrete and more easily applicable. In this compositional software design tutorial I will explain the core of these ideas.

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 Design?

Before diving into the finer details of compositional software design, I want to give you a bit of history of where I got the idea from.

Compositional software design is my attempt at reverse engineering and distill the design thinking that went into the creation of Design Patterns, Dependency Injection, Functional Programming, and other similar design techniques.

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

By itself there is not really a problem with this process. The problem is, that we tend to name and then memorize the solutions - and then try to reuse those same solutions over and over again in different contexts. The catalogue of design patterns is a great example of this.

What I would like to try instead is - not to reuse the design solutions - but to reuse the design thinking. This should help us produce solutions that are specifically adapted to each specific situation. In different words - to change from looking for best practices to looking for appropriate practices.

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.

Compositional Design Principles

Designing for composability requires shaping the code in ways that makes it easier to

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. 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. 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.).

Reasons to Split

In Compositional Design there are three major reasons to split a unit of code into multiple units:

The Magic is in the Split

The magic of software design is in how you split your code up into smaller units.

Other design ideas such as design patterns, hexagonal architecture, clean code etc. focus more on what you split your code into. In other words, the focus is on the end result - the final structure.

Compositional design, on the other hand, is focused on why you split your code. Compositional design is not so worried about the name of the final structure. The final structure tends to materialize by itself, when you just keep splitting your code according to your splitting reasoning - your design reasoning.

The focus on why you split your code, instead of what you split your code into, means that the compositional design philosophy does not care if you implement a given design idea or design pattern 100% according to its description in some book. The design pattern is not the goal. The reason you make the split in the first place - is the goal - and if your only goal is to use some design idea or design pattern - you are not doing compositional design.

Jakob Jenkov

Featured Videos

Java ForkJoinPool

P2P Networks Introduction




















Advertisements

High-Performance
Java Persistence
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