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
- Hardcoded 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.
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
It starts with Execution.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.
Determining flow with ChainAction.java
public enum ChainAction {
CONTINUE, STOP
}
This is a self-explanatory enum that declares two possible actions after a step completes:
- Go to next step
- Stop the current chain as is
A custom wrapper ChainException.java
public class ChainException extends RuntimeException {
// Other fields can be added for metadata
}
Remembering everything with ChainContext.java
public class ChainContext {
private final Map<String, Object> values;
public ChainContext() {
this.values = new HashMap<>();
}
public <T> void setValue(String key, T value) {
values.put(key, value);
}
public Object getValue(String key) {
return values.get(key);
}
@SuppressWarnings("unchecked")
public <T> T getValue(String key, Class<T> clazz) {
Objects.requireNonNull(clazz);
return (T) values.get(key);
}
}
This class is a crucial core abstraction that 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<String, Object>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 of the chain, we can define a class that orchestrates the entire execution.
public class Chain {
private final String name;
private final List<Executable> 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);
}
}
Let's break it down:
- We have a
namefor the chain and aList<Executable>to store all the executables in a sequence. - We define a Fluent API for creating a chain using the static
of(String name)andnext(Executable executable)andcontext(ChainContext context)methods - We can then run this chain using the
run()method, which iterates over all the executables and executes them - We also have a helper method to visualise the chain with all its steps
Notice that each Executable knows nothing about what comes before or after it. The chain is responsible for orchestration; executables focus purely on their logic.
And that's it! That's all it takes to implement the most barebones Chain Execution Engine.
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(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
Notice how the
ChainContextis being used to transfer data between Executables within the chain.
What this implementation does NOT handle
- Other chain actions like
SKIP,ROLLBACK,RETRY. These would offer more functionality- Lifecycle hooks for chain and executables, e.g.
onFailure(),before(),after()- Parallel execution of steps
- A Chain Registry which 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 made 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.

Top comments (0)