Workflows and Activities
Broadleaf provides configurable workflows for key phases of the eCommerce process, namely: Checkout, pricing, and cart operations. These workflows are represented in XML in the Spring application context file. At the most basic level, Broadleaf provides a default configuration for checkout that covers the basic steps, using simple modules. Most users will want to override part, or all, of the steps defined in these default configurations to fit their own needs and business rules. We'll start out by describing the default configuration, and then advance later into customization strategies.
Anatomy of a workflow
<!-- Pricing Workflow configuration -->
<bean p:order="1000" id="blOfferActivity" class="org.broadleafcommerce.core.pricing.service.workflow.OfferActivity" />
<bean p:order="2000" id="blConsolidateFulfillmentFeesActivity" class="org.broadleafcommerce.core.pricing.service.workflow.ConsolidateFulfillmentFeesActivity" />
<bean p:order="3000" id="blFulfillmentItemPricingActivity" class="org.broadleafcommerce.core.pricing.service.workflow.FulfillmentItemPricingActivity" />
<bean p:order="4000" id="blFulfillmentGroupMerchandiseTotalActivity" class="org.broadleafcommerce.core.pricing.service.workflow.FulfillmentGroupMerchandiseTotalActivity" />
<bean p:order="5000" id="blFulfillmentGroupPricingActivity" class="org.broadleafcommerce.core.pricing.service.workflow.FulfillmentGroupPricingActivity" />
<bean p:order="6000" id="blShippingOfferActivity" class="org.broadleafcommerce.core.pricing.service.workflow.ShippingOfferActivity" />
<bean p:order="7000" id="blTaxActivity" class="org.broadleafcommerce.core.pricing.service.workflow.TaxActivity" />
<bean p:order="8000" id="blTotalActivity" class="org.broadleafcommerce.core.pricing.service.workflow.TotalActivity" />
<bean p:order="9000" id="blAdjustOrderPaymentsActivity" class="org.broadleafcommerce.core.pricing.service.workflow.AdjustOrderPaymentsActivity" />
<bean id="blPricingWorkflow" class="org.broadleafcommerce.core.workflow.SequenceProcessor">
<property name="processContextFactory">
<bean class="org.broadleafcommerce.core.pricing.service.workflow.PricingProcessContextFactory"/>
</property>
<property name="activities">
<list>
<ref bean="blOfferActivity" />
<ref bean="blConsolidateFulfillmentFeesActivity" />
<ref bean="blFulfillmentItemPricingActivity" />
<ref bean="blFulfillmentGroupMerchandiseTotalActivity" />
<ref bean="blFulfillmentGroupPricingActivity" />
<ref bean="blShippingOfferActivity" />
<ref bean="blTaxActivity" />
<ref bean="blTotalActivity"/>
<ref bean="blAdjustOrderPaymentsActivity"/>
</list>
</property>
<property name="defaultErrorHandler">
<bean class="org.broadleafcommerce.core.workflow.DefaultErrorHandler">
<property name="unloggedExceptionClasses">
<list>
<value>org.hibernate.exception.LockAcquisitionException</value>
</list>
</property>
</bean>
</property>
</bean>
Every workflow is an instance of the SequenceProcessor
class from Broadleaf. This class manages the orderly activation of subordinate activities as well as handling error states, should they occur. Workflows are configured in three main areas: The process context factory, the activities list, and the rollback handler.
ProcessContextFactory
The processContextFactory
property must be set with an instance of a class implementing the ProcessContextFactory
interface. All such implementers are responsible for creating an instance of a class implementing the ProcessContext
interface (more on this in a bit). In our example, the PricingProcessContextFactory
class is used, which creates an instance of the ProcessContext<Order>
.
ProcessContexts
are usually only relevant for a particular type of workflow, and, thus, there is usually a differentProcessContextFactory
for each workflow as well. An immediate exception to this rule is that each of the cart workflows (add, remove, update) uses the sameProcessContext
and thus the sameProcessContextFactory
since they are so similar.
Activities
The activities
property must be filled with a list containing one or more Activities
. Each Activity
in the workflow is responsible for performing a single unit of work. (Examples include computing tax in the TaxActivity
or summing up the total price of an order in the TotalActivity
.) Composite activities can contain a subordinate workflow, allowing us to create complex, nested workflows.
Error Handler
The error handler is an ErrorHandler
instance passed to the defaultErrorHandler
property. By default, we specify the blDefaultErrorHandler
, which is a simple error handler bean defined in the Broadleaf Commerce application context. This default error handler only serves to log the exception to system.out
and then bubbles the exception--stopping this workflow and its processing of activities for the current thread.
Activity Ordering
If you look back at that initial reference to the blPaymentWorkflow
at the beginning of this document, you'll notice a slightly peculiar configuration for the activity:
<bean p:order="1000" id="blOfferActivity" class="org.broadleafcommerce.core.pricing.service.workflow.OfferActivity" />
The blOfferActivity
has a p:order
property defined. This will set the order
property for this Activity
, which determines how the framework will order these activities in relation to other activities. This is really handy when combined with the fact that Broadleaf merges activity lists together within the same bean id. Let's look at a more complicated example of ordering an action with the blUpdateItemWorkflow
:
<bean p:order="1000" id="blVerifyCustomerMaxOfferUsesActivity" class="org.broadleafcommerce.core.offer.service.workflow.VerifyCustomerMaxOfferUsesActivity"/>
<bean p:order="2000" id="blPaymentServiceActivity" class="org.broadleafcommerce.core.checkout.service.workflow.PaymentServiceActivity"/>
<bean p:order="3000" id="blRecordOfferUsageActivity" class="org.broadleafcommerce.core.offer.service.workflow.RecordOfferUsageActivity"/>
<bean p:order="4000" id="blCompleteOrderActivity" class="org.broadleafcommerce.core.checkout.service.workflow.CompleteOrderActivity"/>
<bean id="blCheckoutWorkflow" class="org.broadleafcommerce.core.workflow.SequenceProcessor">
<property name="processContextFactory">
<bean class="org.broadleafcommerce.core.checkout.service.workflow.CheckoutProcessContextFactory"/>
</property>
<property name="activities">
<list>
<ref bean="blVerifyCustomerMaxOfferUsesActivity" />
<ref bean="blPaymentServiceActivity" />
<ref bean="blRecordOfferUsageActivity" />
<ref bean="blCompleteOrderActivity" />
</list>
</property>
<property name="defaultErrorHandler" ref="blDefaultErrorHandler"/>
</bean>
In all Broadleaf workflows each framework-defined activity goes up by 1000, allowing you to order your own activities in-between. For instance, if you want to put a custom activity in-between the recording offer usage and marking the order as completed, you would define the following in your applicationContext-mycompany.xml
:
<bean id="blCheckoutWorkflow" class="org.broadleafcommerce.core.workflow.SequenceProcessor">
<property name="activities">
<list>
<bean p:order="3500" class="com.mycompany.core.workflow.DecrementInventoryActivity" />
</list>
</property>
</bean>
A few important notes about this activity ordering:
- If 2 activities have the exact same order (for instance, both are configured with '3000'), then the ordering will be in-place. This means that the ordering will be determined by applicationContext merge ordering (as defined in the
patchConfigLocations
inweb.xml
). - Integration modules should declare their activity ordering in the 100 range (like 3100, 3200, etc.) so that specific implementations can further weave custom activities in-between those as well.
- All framework activity ordering can be overridden by referencing the bean id and changing the
p:order
attribute. - If you have not configured your activity to have an explicit ordering, your activity will be placed at the end of the workflow (more explicitly, the default order for activities is
Ordered.LOWEST_PRECEDENCE
).
Rollback Handlers
Rollback handlers provide a way for an activity to register state, and possibly rollback an operation at some later time. Consider this example:
One of the first steps of the checkout workflow would be to validate and confirm any payments on the Order. After payments have been confirmed and charged, a following step may update the cart status or send a notification to a downstream system. Now, if for some reason the cart status update step fails, it would be useful to have a standardized way to refund or void the credit card charge made previously. This is the purpose of the rollback handler.
Rollback handlers can perform any custom actions and operate based on the state that is passed in. As a result, the rollback operation doesn't have to be to an external service like a payment gateway. It could instead be a compensating transaction that returns the database state back to what it was before to the activity's execution. This is an important distinction as most workflows themselves are not executed under a JDBC transaction, since the workflow lifecycle length makes keeping transactions open impractical. Therefore, because the whole workflow is not under a single transaction, there will not be an immediate and automatic rollback of database state upon an exception, which means that a rollback handler must be used to explicitly set the database back to the desired state.
The simplest form of rollback handler is configured via the XML application context in the activity declaration for the workflow. Let's look at the checkout workflow declaration:
<bean p:order="1000" id="blVerifyCustomerMaxOfferUsesActivity" class="org.broadleafcommerce.core.offer.service.workflow.VerifyCustomerMaxOfferUsesActivity"/>
<bean p:order="2000" id="blValidateProductOptionsActivity" class="org.broadleafcommerce.core.checkout.service.workflow.ValidateProductOptionsActivity"/>
<bean p:order="3000" id="blValidateAndConfirmPaymentActivity" class="org.broadleafcommerce.core.checkout.service.workflow.ValidateAndConfirmPaymentActivity">
<property name="rollbackHandler" ref="blConfirmPaymentsRollbackHandler" />
</bean>
<bean p:order="4000" id="blRecordOfferUsageActivity" class="org.broadleafcommerce.core.offer.service.workflow.RecordOfferUsageActivity">
<property name="rollbackHandler" ref="blRecordOfferUsageRollbackHandler" />
</bean>
<bean p:order="5000" id="blCommitTaxActivity" class="org.broadleafcommerce.core.checkout.service.workflow.CommitTaxActivity">
<property name="rollbackHandler" ref="blCommitTaxRollbackHandler" />
</bean>
<bean p:order="6000" id="blDecrementInventoryActivity" class="org.broadleafcommerce.core.checkout.service.workflow.DecrementInventoryActivity">
<property name="rollbackHandler" ref="blDecrementInventoryRollbackHandler" />
</bean>
<bean p:order="7000" id="blCompleteOrderActivity" class="org.broadleafcommerce.core.checkout.service.workflow.CompleteOrderActivity">
<property name="rollbackHandler" ref="blCompleteOrderRollbackHandler" />
</bean>
<bean id="blCheckoutWorkflow" class="org.broadleafcommerce.core.workflow.SequenceProcessor">
<property name="processContextFactory">
<bean class="org.broadleafcommerce.core.checkout.service.workflow.CheckoutProcessContextFactory"/>
</property>
<property name="activities">
<list>
<ref bean="blVerifyCustomerMaxOfferUsesActivity" />
<ref bean="blValidateProductOptionsActivity" />
<ref bean="blValidateAndConfirmPaymentActivity" />
<ref bean="blRecordOfferUsageActivity" />
<ref bean="blCommitTaxActivity" />
<ref bean="blDecrementInventoryActivity" />
<ref bean="blCompleteOrderActivity" />
</list>
</property>
<property name="defaultErrorHandler">
<bean class="org.broadleafcommerce.core.workflow.DefaultErrorHandler">
<property name="unloggedExceptionClasses">
<list>
<value>org.broadleafcommerce.core.inventory.service.InventoryUnavailableException</value>
</list>
</property>
</bean>
</property>
</bean>
In the out-of-the-box Checkout workflow, notice we include several rollback handlers that will execute if there is an exception in any of the subsequent activities. (Note: An activity's own rollback handler is not called when that activity fails, just the rollback handlers of those activities that have already succeeded previously in the workflow. It is the activity's responsibility to rectify its own state, if applicable, when it fails). In the example above, if the cart update step fails, the blConfirmPaymentsRollbackHandler
will get involved, which will call the configured Payment Gateway to either refund or void the payment based on implementation.
RollbackHandler Interface
All rollback handlers implement the RollbackHandler
interface, which has a single method that must be implemented:
public void rollbackState(Activity<? extends ProcessContext> activity, ProcessContext processContext, Map<String, Object> stateConfiguration) throws RollbackFailureException;
The contract contains all the information that should be necessary to perform the compensating transaction. The activity in question, as well as the ProcessContext
(see next section) are provided. Most important, the stateConfiguration
Map is provided, which contains important information about the operation that was previously performed by the activity. This state information can be used to help reverse the operation. The stateConfiguration
is generally provided by the activity during execution, which we'll look at next.
ActivityStateManager
The ActivityStateManager
is a flexible component used to register a rollback handler and any accompanying state. The ActivityStateManager
can be called from anywhere in the flow of execution for a workflow (Activity, Module, ErrorHandler, etc...). To get a handle on the singleton ActivityStateManager
instance, execute:
ActivityStateManagerImpl.getStateManager();
The ActivityStateManager
interface provides several methods for registering rollback handlers and state. In fact, you can even further refine rollback behavior by calling the overloaded version of registerState()
that accepts a region parameter. A region allows you to provide a grouping label for one or more rollback handlers, which provides an avenue for selectively rolling back a subset of registered rollback handlers. ActivityStateManager
also has methods for clearing state and for executing rollbacks, which may also be called at any time (although, you'll generally let the system call these latter methods for you automatically).
Implicit vs Explicit Registration
By default, one must explicitly register a rollback handler in code (see above). This is because most rollback handlers will require thread specific state to be registered in order to achieve a viable rollback state. However, for simpler rollback handlers, if no thread specific state is required to achieve a rollback, then the system can be configured for implicit/automatic rollback registration. For example:
<bean id="myActivity" class="com.test.MyActivity">
<property name="rollbackHandler" ref="blTestRollbackHandler"/>
<property name="stateConfiguration">
<map>
<entry key="testProperty" value="testValue"/>
</map>
</property>
<property name="automaticallyRegisterRollbackHandler" value="true"/>
</bean>
In this example, we're specifying a rollback handler, some static state and telling the system to automatically register the rollback handler. Because we don't care about thread specific state here, this configuration is all that's required to register the rollback handler with the ActivityStateManager
singelton.
Implicit vs Explicit Rollback
By default, registered RollbackHandler instances are automatically executed upon exception in the workflow execution. However, there may be cases where it is advantageous to turn off this behavior and instead to call rollback explicitly from ActivityStateManager
in code (e.g., you might want to rollback a specific region, instead of all rollback handlers). To engage explicit rollback behavior, declare:
workflow.auto.rollback.on.error=false
in your runtime environment configuration property files. Note: This behavior can be overridden for each workflow by setting the autoRollbackOnError
property for each workflow in your app context XML.
ProcessContext
A ProcessContext
is a common context object that is passed in and out of every activity in a workflow. This context object usually contains data pertinent to the theme of the workflow. Each ProcessContext
provides methods to set and check the state of the workflow using stopProcess()
and isStopped()
methods, respectively. For example, by calling stopProcess()
directly, an activity could stop further processing of a workflow without necessarily raising an exception.
Conditional Activity Execution
The Activity
interface provides support for skipping over that activity based on the items in the ProcessContext
. This method is invoked before executing each Activity
. This could prevent duplication of workflow definitions; perhaps a single large workflow could be declared for multiple configurations of a ProcessContext
.
Activities
Activities in their most basic form are instances of the Activity
interface, which provides simple entry points for executing the activity and retrieving the error handler. Most activities will actually implement the BaseActivity
abstract class.
An Activity
is also only relevant for a certain type of workflow (meaning it can only operate on a certain type of ProcessContext
) defined by Java generics. For instance, this would the the definition of an activity in the blPricingWorkflow
:
public class TotalActivity extends BaseActivity<PricingContext> {
@Override
public PricingContext execute(PricingContext context) throws Exception {
Order order = context.getSeedData();
// compute all totals for the order
return context;
}
}
Whereas this Activity
would be defined in the blCheckoutWorkflow
:
public class CompleteOrderActivity extends BaseActivity<CheckoutContext> {
@Override
public CheckoutContext execute(CheckoutContext context) throws Exception {
CheckoutSeed seed = context.getSeedData();
seed.getOrder().setStatus(OrderStatus.SUBMITTED);
seed.getOrder().setOrderNumber(new SimpleDateFormat("yyyyMMddHHmmssS").format(SystemTime.asDate()) + seed.getOrder().getId());
seed.getOrder().setSubmitDate(Calendar.getInstance().getTime());
return context;
}
}
Composite activities also extend BaseActivity
; thus, they are valid candidates for members of the activity list in the workflow definition. However, they differ in the fact that they accept a child workflow as their sole configuration. Whenever a composite activity is called by its parent workflow, that call is propagated down into the child workflows where its activities will be called in an orderly fashion. All child workflows are subject to the same ProcessContext
instance and all exceptions are bubbled to the top and stopProcess()
calls at any level stop the entire nested workflow. By utilizing composite activities, we can achieve more complicated, nested behavior in our workflows.
Error Handlers
public interface ErrorHandler extends BeanNameAware {
public void handleError(ProcessContext context, Throwable th) throws WorkflowException;
}
Error handlers are instances of the simple ErrorHandler
interface (show above). The error handler provides the Broadleaf user an opportunity to perform some task when an exception takes place during the execution of a workflow. This could be as simple as logging the exception, or perhaps something more complicated like releasing resources. As stated earlier, all default Broadleaf workflows use the DefaultErrorHandler
class, which simply logs the exception to System.out
and bubbles the exception.
Removing a Broadleaf Workflow
It is possible that you will want to remove one of the workflows that we have defined in the framework in order to perform more exotic functions or to handle very specific use cases. While we would not normally recommend this, we have provided an easy subclass for you to use: The EmptySequenceProcess
. If you wanted to remove the blCheckoutWorkflow
because you have subclassed OrderService
and have your own implementation of performCheckout()
, simply add this to your application context:
<bean id="blCheckoutWorkflow" class="org.broadleafcommerce.core.workflow.EmptySequenceProcessor" />
Provided Workflows
Below are some of the items that Broadleaf has workflow concepts for out of of the box (full applicationContext definition):
Workflow Bean ID | Description |
---|---|
blAddItemWorkflow | Used when an item is added to the cart |
blUpdateItemWorkflow | Used when an item is updated in the cart |
blRemoveItemWorkflow | Used when an item is removed from the cart |
blPricingWorkflow | Used by blPricingService (which is used by OrderService ) to price an Order |
blCheckoutWorkflow | Invoked by blCheckoutService in order to complete checkout for an Order (charge payments, decrement inventory, change status to SUBMITTED, etc.) |