My past roles required me to write three different applications having surprisingly similar requirements. These requirements were:
- The application must run as a service which receives some form of request
- Once received, several stages of processing needs to be performed on the request (i.e. validation, persistence, state management, etc)
- These stages of processing may change overtime as new requirements come in
In order to achieve this required flexibility, adding new processing logic or modifying existing ones should not affect the code of other tasks. For example: if I add a new logic for validation, my existing code for state management should not be affected.
I need to encapsulate each logic as individual units of "tasks". This way, changes to each task is independent from the other.
The behavior of my service is now defined by the sequence of tasks needed to be performed. This behavior can easily be changed by adding, removing, or modifying tasks in the sequence.
Essentially, I needed to build a workflow.
My design had several iterations throughout the years. My initial design was pretty straightforward: I implemented a workflow as a container of one or more tasks arranged in a sequence. When executed, the workflow iterates through each task and executes it. The workflow has no knowledge of what each task would do, all it knows is that it is executing some task. This was made possible because each task share a common interface. This interface exposes an execute() method which accepts the request as a parameter.
Through dependency injection, the behavior of the workflow becomes very easy to change. I can add, remove and rearrange tasks via configuration.
While proven to be effective, my initial design was application specific -- the workflow can only accept a specific type of request. This makes it an ineffective framework because it only works for that particular application. There was also the problem of tasks not being able to share information: as a result, temporary values needed to be stored within the request itself.
I had the chance to improve on this design on a later project. By applying Generics, I was able to redesign my workflow so that it can accept any type of request.
In order for information to be shared across each tasks, a data structure serving as the workflow "context" is also passed as a parameter to each task.
|Figure 1: Workflow and Tasks|
More often than not, different workflows need to be executed for different types of request. For example, one workflow needs to be executed to open an account, another workflow to close an account, etc.
I came up with a "workflow factory" as a solution to this requirement. Depending on the type of request, the factory will assemble the workflows required to be executed against the request. The factory is exposed as an interface so that I can have different implementations depending on the requirement.
|Figure 2: Workflow Factory|
My service now becomes very simple: as soon as a request comes in, I will call the factory to assemble the required workflows then one-by-one execute my request against each of them.
|Figure 3: Workflow Service|
Flowee is an open source implementation of the above design. Having gone through several iterations, I feel that this design has matured enough to warrant a common framework that can be useful to others.
The project is hosted via GitHub at https://github.com/jramoyo/flowee.
Workflow and Tasks
The core framework revolves around the Workflow interface and its relationship with the Task interface.
|Figure 4: Flowee Workflow|
The execute() method of Workflow accepts both the request and an instance of WorkflowContext. Errors encountered while processing the request are thrown as WorkflowExceptions.
AbstractWorkflow provides a generic implementation of Workflow. It iterates through a list of associated Tasks to process a request. Application specific workflows will inherit from this class.
Most workflow Tasks will inherit from AbstractTask. It provides useful features for building application specific tasks namely:
- Retry task execution when an exception is encountered
- Silently skip task execution instead of throwing an exception
- Skip task execution depending on the type of request
The WorkflowFactory is another important piece of the framework. The way it abstracts the selection and creation of workflows simplifies the rest of the core components.
|Figure 5: Flowee WorkflowFactory|
AbstractConfigurableWorkflowFactory is used to build configuration driven workflow factories. It defines an abstract fetchConfiguration() method and an abstract fetchTaskNames() method that sub-classes need to implement. These methods are used to fetch configurations from various sources: either from the file system or from a remote server.
The configuration is represented as a Map whose key is the name of a workflow and whose value is the condition which activates that workflow.
AbstractConfigurableWorkflowFactory uses a Filter instance to evaluate the conditions configured to activate the workflows.
AbstractPropertiesWorkflowFactory is a sub-class of AbstractConfigurableWorkflowFactory that fetches configuration from a properties file.
WorkflowService and AbstractWorkflowService acts as a facade linking the core components together.
|Figure 6: Flowee WorkflowService|
While most applications will interact with workflows through the WorkflowService, those requiring a different behavior can interact with the underlying components directly.
The primary purpose of Flowee is to provide groundwork for rule-driven workflow selection and execution. Developers can focus the majority of their efforts on building the tasks which hold the actual business requirements.
Workflows built on Flowee will run without the need for "containers" or "engines". The framework is lightweight and integrates seamlessly with any application.
This post discussed the design considerations which led to the implementation of Flowee. It also described the structure of the core framework. My next post will demonstrate how easy it is to build workflow-based services with Flowee by going through a sample application.