Building an Executable Chain to Understand Composable Systems

Published: (January 18, 2026 at 12:44 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Inspiration

While exploring frameworks like LangChain, I noticed a recurring idea: instead of writing monolithic logic, execution is broken into composable steps that can be chained, reused, and rearranged.

This article is not about LangChain itself, but about understanding that underlying idea by building a minimal executable chain abstraction from scratch.


Why do this?

When working without a mental model for execution chains, one often runs into the pain of:

  • Deeply nested conditionals
  • Hard‑coded execution order
  • Monolithic “do‑everything” methods

As a result, the entire chain of operations becomes brittle, hard to test, and difficult to rearrange.

If we instead think of each step as a unit that does exactly one thing, we arrive at the basic building block of a chain.

With that mental model in place, let’s design the smallest possible abstraction to support it.


What is an Executable Chain?

As the name suggests, an executable chain is a sequence of small, focused units of logic that can be arranged—or rearranged—to achieve a larger unit of work.

Take a look at a simple Order Processing chain below.

Order Flow Chain

Each step handles one clear process and the chain is executed sequentially.

An Executable is something that:

  • Takes input
  • Performs work
  • Returns output

and it is:

  • Composable
  • Testable
  • Predictable

Designing the Core Abstraction

Executable.java

@FunctionalInterface
public interface Executable {
    ChainAction execute(ChainContext context) throws ChainException;
}

This is the building block of a chain. We wrap our logic inside the execute method and use the ChainContext to track the current state of the chain. The method returns a ChainAction, which determines whether the chain should proceed to the next step or stop execution.

Returning a ChainAction makes control flow explicit and keeps individual executables unaware of the larger chain structure.


ChainAction.java

public enum ChainAction {
    CONTINUE,
    STOP
}

A self‑explanatory enum that declares two possible actions after a step completes:

  • CONTINUE – go to the next step
  • STOP – halt the current chain as is

ChainException.java

public class ChainException extends RuntimeException {
    // Other fields can be added for metadata
}

A custom unchecked exception that can be enriched with additional metadata if needed.


ChainContext.java

public class ChainContext {
    private final Map values;

    public ChainContext() {
        this.values = new HashMap<>();
    }

    public  void setValue(String key, T value) {
        values.put(key, value);
    }

    public Object getValue(String key) {
        return values.get(key);
    }

    @SuppressWarnings("unchecked")
    public  T getValue(String key, Class clazz) {
        Objects.requireNonNull(clazz);
        return (T) values.get(key);
    }
}

This class generalises what enters an Executable and what leaves it.
We pass inputs by setting them as key‑value pairs and similarly handle outputs.

NOTE: Using a Map trades type safety for flexibility. This is a deliberate choice to keep the abstraction simple and generic.


Controlling it all with Chain.java

Now that we have defined the building blocks, we can create a class that orchestrates the entire execution.

public class Chain {
    private final String name;
    private final List executables;
    private ChainContext context;

    private Chain(String name) {
        this.name = name;
        this.executables = new ArrayList<>();
        this.context = new ChainContext();
    }

    public static Chain of(String name) {
        Objects.requireNonNull(name);
        return new Chain(name);
    }

    public Chain next(Executable executable) {
        executables.add(executable);
        return this;
    }

    public Chain context(ChainContext context) {
        this.context = context;
        return this;
    }

    public void run() throws ChainException {
        System.out.println("Executing Chain : " + name);
        for (var e : executables) {
            ChainAction state = e.execute(context);
            if (state == ChainAction.STOP) break;
        }
    }

    public void printChain() {
        var s = executables.stream()
                .map(e -> e.getClass().getSimpleName())
                .collect(Collectors.joining(" --> "));
        System.out.println(s);
    }
}

Breakdown

  • name – identifier for the chain.
  • executables – ordered list of steps.
  • Fluent APIof(String), next(Executable), and context(ChainContext) let you build a chain in a readable way.
  • run() – iterates over the executables, respecting the ChainAction each step returns.
  • printChain() – visualises the chain as StepA --> StepB --> StepC.

Notice that each Executable knows nothing about what comes before or after it. This loose coupling is what makes the chain composable, testable, and easy to rearrange.

Putting it Together

Defining a constants class

public class GenericConstants {
    public static final String ORDER_ID = "orderId";
    public static final String CURRENT_STATE = "currentState";
}

Defining the steps

public class ValidateOrder implements Executable {

    @Override
    public ChainAction execute(ChainContext context) throws ChainException {
        System.out.println("Executing ValidateOrder");
        context.setValue(GenericConstants.CURRENT_STATE, "ValidateOrder Done!");
        return ChainAction.CONTINUE;
    }
}

public class CheckInventory implements Executable {

    @Override
    public ChainAction execute(ChainContext context) throws ChainException {
        System.out.println("Executing CheckInventory");
        System.out.println("    Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
        context.setValue(GenericConstants.CURRENT_STATE, "CheckInventory Done!");
        return ChainAction.CONTINUE;
    }
}

public class ReserveStock implements Executable {

    @Override
    public ChainAction execute(ChainContext context) throws ChainException {
        System.out.println("Executing ReserveStock");
        System.out.println("    Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
        context.setValue(GenericConstants.CURRENT_STATE, "ReserveStock Done!");
        return ChainAction.CONTINUE;
    }
}

public class TakePayment implements Executable {

    @Override
    public ChainAction execute(ChainContext context) throws ChainException {
        System.out.println("Executing TakePayment");
        System.out.println("    Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
        context.setValue(GenericConstants.CURRENT_STATE, "TakePayment Done!");
        return ChainAction.CONTINUE;
    }
}

public class SendNotification implements Executable {

    @Override
    public ChainAction execute(ChainContext context) throws ChainException {
        System.out.println("Executing SendNotification");
        System.out.println("    Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
        System.out.println("    Order with Id: " + context.getValue(GenericConstants.ORDER_ID) + " processed!");
        context.setValue(GenericConstants.CURRENT_STATE, "SendNotification Done!");
        return ChainAction.CONTINUE;
    }
}

Running the Chain

public class Main {
    public static void main(String[] args) {
        Chain chain = Chain.of("order-process-chain")
                .next(new ValidateOrder())
                .next(new CheckInventory())
                .next(new ReserveStock())
                .next(new TakePayment())
                .next(new SendNotification());

        ChainContext context = new ChainContext();
        context.setValue(GenericConstants.ORDER_ID, "ORD-123");
        chain.context(context);
        chain.run();

        System.out.println();
        chain.printChain();
    }
}

/* Output:
Executing Chain : order-process-chain
Executing ValidateOrder
Executing CheckInventory
    Previous State : ValidateOrder Done!
Executing ReserveStock
    Previous State : CheckInventory Done!
Executing TakePayment
    Previous State : ReserveStock Done!
Executing SendNotification
    Previous State : TakePayment Done!
    Order with Id: ORD-123 processed!

ValidateOrder --> CheckInventory --> ReserveStock --> TakePayment --> SendNotification
*/

Note: The ChainContext is used to transfer data between Executables within the chain.


What this implementation does NOT handle

  • Other chain actions such as SKIP, ROLLBACK, RETRY (which would add more functionality).
  • Lifecycle hooks for chains and executables, e.g., onFailure(), before(), after().
  • Parallel execution of steps.
  • A Chain Registry that stores chains based on their names.

Final Words

Creating simple abstractions is both fun and educational. They force you to confront trade‑offs that are often hidden behind polished frameworks.

Building this executable chain makes it clear that many modern systems — from workflow engines to AI pipelines — rely on the same underlying ideas. Once you understand the abstraction, the framework feels far less magical.

Back to Blog

Related posts

Read more »

How to copy Free Fire Name Copy by UID

!Free Fire Name Copy Toolhttps://media2.dev.to/dynamic/image/width=800,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws...