[Software Architecture] The Pipeline Design Pattern — From Zero to Hero

Guillaume Bonnot
5 min readApr 4, 2020

--

In this article, we will explore the different ways to implement the pipeline design pattern starting from the basics to more complex solutions.

The Pipeline Pattern

The pipeline pattern is a software design pattern that provides the ability to build and execute a sequence of operations.

https://www.hojjatk.com/2012/11/chain-of-responsibility-pipeline-design.html

This pattern is better used in conjunction with the plugin pattern, to dynamically build the pipeline when the application starts.

Sequence

The most basic implementation of a pipeline would be a simple sequence of operations.

The interface of an operation can be invoked to process data.

The pipeline processes each operation one by one. The pipeline class is also implementing the IOperation interface so they can be combined.

The operation can be written in a dedicated class.

Or use a wrapper to automatically create an operation from a lambda.

The pipeline operations should be registered before the pipeline is invoked.

Circuit Breaker

The first feature you want to add to your pipeline is to add a circuit breaker.

Each operation will return the result : fail or success.

If an operation fails, the pipeline execution should stop.

Asynchronous

Another requirement could be to have a pipeline that can deal with asynchronous operations.

Every operation will now have to call the next operation in pipeline, after they are finished processing the data.

The pipeline is a bit more complex, because it need to set the next operation when a new operation is registered. Another solution is to use a builder.

This operation is asynchronous and will run in a dedicated thread, when the time is out, it will invoke the next operation to continue the pipeline.

The generic operation can be used both with a simple action, or use a built-in circuit-breaker if you use a function.

This simple example uses several of the features we implemented.

Now you know how to make your pipeline asynchronous !

It would be even better if had another callback to the previous operation so you could have the result going counter-flow through the pipeline.

Plugin

The main reason to use the pipeline design patter is often the requirement to be able to add plugins that will either append operations to an existing pipeline or to hook an operation in the middle of it.

The pipeline is really basic, but this time, the operations are exposed.

Let’s take a simple application with a pipeline that will only display the 3 steps in the console. This application also supports plugins to modify the pipeline.

The first plugin will hook another operation in the second slot of the pipeline.

The second plugin will append a new operation at the end of the pipeline.

The application and the plugins are put together, we can invoke the pipeline.

Batch

Another useful feature is to be able to process batch data in the same pipeline than single items.

The batch pipeline wraps a pipeline and calls every operation on every item.

Every item is wrapped, so we can remember the result of the circuit breaker.

This operation checks if the integer has the required order of magnitude.

The pipeline is going to check the order of magnitude of a batch of integers.

The pipeline only invokes the next operation for items that did not failed.

High Performance Pipeline

The Pipeline Design Pattern can also be referring to a much more specific and performance oriented software architecture.

Some projects use a pipeline to optimize the processing of a huge amount of data by running each operation of the pipeline in a dedicated thread.

Each thread will consume and produce data from a concurrent queue that will act a buffer.

This time, we will use asynchronous operations with a circuit-breaker.

If the operation is successful, it should invoke next, or terminate if failed.

The pipeline is designed to bypass the circuit-breaker. Success or Fail, it will always continue the parent pipeline sequence and call the next operation.

The scenario is bit complex, so I won’t explain everything. The idea is to have different threads to process incoming orders. When an order is finished processing, we check the status of the order.

Each order processor is isolated in a dedicated thread, so you can optimize how you store the data and have direct memory access without using lock.

The payment order processor is the only thread that can access user balances. It can get or update any balance without concern about concurrency issues.

The pipeline is processing the sequence of operations as fast as possible.

Conclusion

The Pipeline Design Pattern has a lot of very different ways to be implemented, from a simple Chain of Command to a more complex Workflow.

Many features can be required such as circuit-breaker, asynchronous or buffer, but most of the time, you want to use pipelines when you want to have plugins that can hook in the pipeline execution flow.

The complete source code for the example can be found on github.

--

--