Building a (Lightweight) Business Process Engine

By

Organizing a set of tasks and activities into a structure to facilitate execution by human or by machine, is one of the best understood ways to create order from chaos. Most people who have made it through primary school understand this, in a primitive “hokey pokey” or “Simon says” sense.

Hokey Pokey anonymous: A place to turn yourself around

And that's what it's all about

This concept of sequence is key to almost all programming languages, which represent flow concepts with language keywords like “if” and “while”. In electronics and robotics, finite state diagrams are used explain and define the desired behavior of a complex system. In the business world, business process flows are used to describe what needs to happen, by whom, to achieve a business goal. It is this third example which I will be focusing on in this piece.

There are existing Java libraries and frameworks which solve the problem of representing and managing business processes, because this is Java. In the Java world, there are existing libraries and frameworks to do pretty much everything. But these libraries are big, and complex, and often insist on being the controlling force across your whole applications. For a lot of cases this is not only unnecessary, but counterproductive.

This article will present a simple, easily customizable, lightweight implementation of a general purpose state machine. It is not intended as a library or a framework, but merely an example of how such a thing can be implemented in an existing system without requiring a complex library or framework.

The Structure

A finite state model (process definition) is made up of a finite number of states, and each of those states has a set of transitions to other states. A process definition will be comprised of states and transitions between those states. Both states and transitions will have an optional “action”, which has a single method to be called when that state or transition is first entered. This is to allow the process to manipulate data in the rest of the system. Based on this very simplistic domain, we can build a simple domain model (click to enlarge):

Business Process Domain Objects

Keeping track of state

The key to making this whole solution work is that the process definition must be kept distinct from the actual, working process instance. You should be able to define “The Hokey Pokey” process once, but have a dozen different people dancing it.

We already have an object which defines the process, so now we need an object to represent a particular instance of that process.

The instance object is really just a token, with no purpose other than to be a unique identifier passed around between the singleton process states. Each time the process gets started, a new instance token is created which is then used to track that specific case.

A token doesn’t have to have any particular logic in it, it could be as simple as calling “new Object()”, but we’re going to make this token a bit more useful. We’re still going to use a POJO as our token, and we’re going to use it to contain some business data. That way this business data will be immediately available to each of the tasks as they are executing, and that same data can be carried along between tasks. If the intention is to integrate the process with an existing system, then that POJO could be a facade to the rest of the system, encapsulating all the required calls out.

The token I’m defining is simply called “InstanceData”. I’ve made it static in this example because I am putting it into the same file as the process itself, but that is not necessary. It can be a normal, non-static class in its own right.

static class InstanceData
{
   // A Unique ID. If we need to persist this process into
   // a datastore, we can use this to retrieve it again,
   // without needing to worry about auto-incrementing database IDs.
   public final String id = UUID.randomUUID().toString();

   // This is a piece of domain data. It will be read,
   // manipulated, and used in decision making throughout the
   // rest of the application.
   public String myValue = "Hello World";
}

Wiring it together

All process definitions inherit from a standard, default process definition class. We could just create a new class in the normal way, but that has some problems. Firstly, it makes it very easy for any part of the code to create new instances of the process definition, something we want to restrict. It also means that developers will be more inclined to change the interface of the class, or override behavior which should not really be overridden.

In fact, the code and structure of the object shouldn’t be changing at all within the containing process definition class. The differences in behaviour should be achieved by changing the tasks and transitions which the definition is composed of.

It would be possible simple to create a “new ProcessDefinition()” then call a sequence of “addState” and “addTransition” methods to build up the class. The downside of this though, is that those methods would still be available after the definition is complete, and we don’t want the definition changing while people are using it! (A builder pattern could solve this, of course, but is not the approach I have taken here.)

So, to help ensure that only one instance of the process exists at a time, we’ll define it as an anonymous inner class. Our “adder” methods can be made protected so our inner class can access them, but other people’s systems can’t. We will use an anonymous constructor to call those adders to build the list of states and transitions. It should almost read like a declarative Domain Specific Language for business processes, written inside the Java code.

ProcessDefinition process = new ProcessDefinition()
{{
	// Because this is an anonymous constructor, I
	// can call any methods from the ProcessDefinition
	//class as protected methods.

	state("Put Body Part In", new Action<InstanceData>()
	{
		public void act(InstanceData myStateObject)
		{
			System.out.println("You put your " + myStateObject.side +
				" " +	myStateObject.extremity + " in.");
		}
	});

	state("Put Body Part Out", new Action<InstanceData>()
	{
		public void act(InstanceData myStateObject)
		{
			System.out.println("You put your " + myStateObject.side +
				" " + myStateObject.extremity + " out.");
		}
	});

	state("Put Body Part In and Shake", new Action<InstanceData>()
	{
		public void act(InstanceData myStateObject)
		{
			System.out.println("You put your " + myStateObject.side +
				" " + myStateObject.extremity +
				" in and you shake it all about.");
		}
	});

	transition(ProcessDefinition.START, "Put Body Part In",
		new Action<InstanceData>()
		{
			@Override
			public void act(InstanceData processInstance)
			{
				processInstance.extremity = "foot";
				processInstance.side = "left";
			}
		});

	transition("Put Body Part In", "Put Body Part Out");

	transition("Put Body Part Out", "Put Body Part In and Shake");

	transition("Put Body Part In and Shake", ProcessDefinition.END);
}};

Making it run

There are a few requirements when it comes to making a process management system actually run.

Firstly, we need to minimize thread abuse. That means that we want to be able to take advantage of parallelism where the execution environment allows for it, but that it should also work in environments with limited threads or thread pooling. In other words, we should closely control the way threads are spawned.

Secondly, execution should use the process definition as though it were configuration, and should not need to change itself to suit different process definitions. The engine which runs the processes should be treated like software infrastructure, and as such should be process-agnostic.

To achieve this, we use a queue. When a process has a token ready to enter a State, it will put that token into this queue. It will then wait there patiently until there is a thread available to execute the behaviour and handle any transitions. That thread will take the token and the State singleton which describes what it needs to do, runs it, then triggers any transitions which are ready to go.

Fortunately, Java support for threading and queues is quite excellent. The BlockingQueue is a class which implements the Queue interface, but its take() method contains thread waiting code. That means that if the queue is empty, the take() method will block until it is ready to complete.

The full State Execution Queue, along with a single execution thread, is described below. This is a real singleton with a static getInstance() method, because when a Transition is run it needs to be able to find the queue to append more task instances

public class StateExecutionQueue implements Runnable
{
	private final BlockingQueue<QueueItem<?>> queue =
		new LinkedBlockingQueue<QueueItem<?>>() ;
	public boolean stop;

	public <T> void addToQueue(State<T> state, T instanceToken)
	{
		queue.add(new QueueItem(state, instanceToken));
	}

	public void run()
	{
		try
		{
			// Allow for a "poison pill" clean exit.
			// To shut down, set "stop" to true, then send a
			// dummy task to the queue to run through the loop.
			while (!this.stop)
			{
				queue.take().execute();
			}
		}
		catch (InterruptedException ex) {}
	}

	private static StateExecutionQueue instance =
		new StateExecutionQueue();

	public static StateExecutionQueue getInstance()
	{
		return instance;
	}
}

The QueueItem class is just a container linking the instance token with the State that needs to be processed. If memory is at a premium in your system, then the instance variable could be serialized to the database, and only a reference stored in the QueueItem container.

public class QueueItem <T>
{
	private State<T> state;
	private T instanceToken;

	public QueueItem(State<T> state, T instanceToken)
	{
		this.state = state;
		this.instanceToken = instanceToken;
	}

	public void execute()
	{
		state.execute(instanceToken);
	}
}

Everybody dance now!

At this point, we’re ready to run our process. Immediately underneath the process definition we coded earlier, add the following:

InstanceData instanceToken = new InstanceData();
process.start(instanceToken);

The first line creates a new instance of the process, simply by instantiation the POJO we’re using as our token. The second line then starts the process executing, using that token.

You put your left foot in.
You put your left foot out.
You put your left foot in and you shake it all about.
Dance!

Yeehaw!

Doing things later

If your process is completely automated, and only needs to run code in the existing system then transition, then this code is now complete. However, you may have noticed a slight weakness in the execution thread. That is, if a state’s Action takes a very long time to complete, it locks the execution thread up. That’s no good if, for example, you need to wait on a human user to do something, or if you need to look something up over a slow web service.

In these cases, you want a state which can be processed asynchronously. That is, the state can be processed by the execution queue quickly so that the queue to continue on to the next waiting token, but the instance token itself mustn’t transition until the asynchronous part of the state has been completed.

This is not actually as hard as it sounds. The current “State” code has execution code which looks like this:

public void execute(T instanceToken)
{
	if(this.action != null)
	{
		action.act(instanceToken);
	}
	transition(instanceToken);
}

If we create a new subclass of State, called AsyncState, and override the execute method, we can make sure that it doesn’t automatically transition itself. Instead, it should add itself to a ‘bucket’ of tasks which are waiting on asynchronous action.

public void execute(T instanceToken)
{
	if(this.action != null)
	{
		action.act(instanceToken);
	}
	AsyncStatePending.getInstance().addToPending(this, instanceToken);
}

This “AsyncStatePending” bucket is really just a singleton wrapping a HashMap. It holds all the tasks waiting for further intervention.

public class AsyncStatePending
{
	private final HashMap<Object, Task<?>> queue =
		new HashMap<Object, Task<?>>();

	public <T> void addToPending(State<T> state, T instanceToken)
	{
		queue.put(instanceToken, state);
	}

	public <T> void removeFromPending(State<T> state, T instanceToken)
	{
		queue.remove(instanceToken);
	}

	public HashMap<Object, Task<?>> getAllPendingTasks()
	{
		return queue;
	}

	private static AsyncStatePending instance =
		new AsyncStatePending();

	public static AsyncStatePending getInstance()
	{
		return instance;
	}
}

We also need a way of telling the AsyncState that we’ve completed the work, and that it can transition now. To do that, we add one more method to the AsyncState class, which removes it from the bucket and tells it to transition as normal.

public void complete(T instanceToken)
{
	AsyncStatePending.getInstance()
		.removeFromPending(this, instanceToken);
	transition(instanceToken);
}

With that, the process is able to support asynchronous states.

What’s missing

There are a few things which could be done to make this project more production-friendly, but which I have not yet implemented:

Multiple execution threads

Despite all my comments about being careful around threading, you’ve probably noticed that I only have a single execution thread. This is really just a matter of reducing complexity though, because the blocking queue is guaranteed thread safe. You can have as many threads as you like listening on that shared queue. This would involve splitting the queue itself from the processing thread, then simply starting multiple processing threads.

Token persistence

The asynchronous tasks are currently being stored in a live, in-memory, hash map. With sufficiently large quantities of data, or very long lived tasks, this could be a problem; restarting the system would result in all in-progress tasks being lost! This is obviously not ideal, but is reasonably simple to rectify. By ensuring that all instance token objects maintain an ID which can be accessed through an interface, adding .equals and .hashCode methods which use the ID (so that hashmap lookups find the correct matches), and providing a way to make those objects are capable of persisting themselves, you could transform the in-memory hash map into an in-database hashmap relatively easily.

The Poison Pill exit

You may have noticed that the execution thread has code in there to support a poison pill exit. The poison pill technique is a way of safely shutting down threads. In this case, when you want to shut town the execution thread, you simply set the “stop” value to true and send a dummy task. When that task is pulled off the queue and processed, the thread will exit its loop and shut down. This code has not yet been written.

What next?

This piece of code is really just a proof-of-concept, but it should give a good indication of how lightweight process management can be integrated into an existing system smoothly, without involving large libraries or frameworks. The full source code is attached, if you would like to try implementing any of the “missing” features yourself, or would just like to have a play with it.

Source code: Business-Process-Engine-Java

Leave a Reply