Development

8 min read

The Engine of Commerce: A Developer's Guide to Broadleaf's Dynamic Pricing and Promotions

Sunny Yu

Written by Sunny Yu

Published on Aug 12, 2025

pricing screen

The ability to offer flexible, targeted, and powerful promotions is a cornerstone of modern eCommerce. A rigid pricing engine can be a significant bottleneck, forcing businesses to rely on manual processes or simple, inflexible discount codes. Broadleaf Commerce solves this problem with a highly extensible, rules-based engine for managing pricing and promotions. This isn't just about applying a coupon; it’s a sophisticated system designed to handle complex business logic, from multi-tiered discounts to personalized promotions and beyond.

This blog post will provide a technical deep dive into the architecture of Broadleaf's pricing and promotions engine. We’ll explore the key components, the declarative rules-based system that powers it, and—most importantly—how developers can extend and customize it to meet the most demanding business requirements with practical, code-centric guidance.

The Architecture: A Clear and Decoupled Workflow

The Broadleaf pricing and promotions workflow is a pipeline composed of distinct, decoupled microservices. Understanding their specific roles is key to building accurate and extensible solutions.

  • pricing-service: This service's primary responsibility is to determine the best price of products. It looks for all applicable PriceLists and their relevant PriceData to match prices to the items in the cart. It operates independently and does not directly deal with offers or promotions.
  • offer-service: This service's sole purpose is to evaluate promotions. It contains the rules engine that determines which offers are applicable to a given Cart. It does not apply the discounts itself.
  • cart-operation-service: This service acts as the orchestrator. It is responsible for making sure the cart and its items are correctly priced. After the pricing-service has set the base prices, the cart-operation-service retrieves all applicable offers from the offer-service and then applies these offers to the Cart, updating the prices and discounts accordingly.

This architectural separation of concerns means you can manage your pricing rules, offers, and cart logic independently. You could, for instance, deploy a new set of promotions by updating only the offer-service without touching the core pricing-service.

The Rules-Based Promotions Engine: A Declarative Approach

Broadleaf’s promotion system is powered by a declarative rules engine. Instead of writing hard-coded logic for every promotion, you define a set of rules that the engine evaluates at runtime. Each promotion has three key parts:

  1. Offer Type: What kind of promotion is it? (e.g., ORDER_PERCENT_DISCOUNT, ITEM_FIXED_DISCOUNT). This determines the action to be taken.
  2. Offer Target: What does the promotion apply to? (e.g., ORDER, ITEM, SHIPPING). This specifies the scope of the promotion.
  3. Offer Rules: The conditions that must be met for the offer to be valid. These are the most powerful parts of the engine, allowing for complex logic like "customer is in a specific segment" or "cart subtotal is greater than $100."

Here's a simplified view of how an offer for "10% off an order over $100" might be structured:

{

  "offerType": "ORDER_PERCENT_DISCOUNT",

  "value": 10,

  "offerRule": {

    "target": "ORDER",

    "conditions": [

      {

        "type": "ORDER_SUBTOTAL_GREATER_THAN",

        "value": 100.00

      }

    ]

  },

  "redeemableCode": "SAVE10",

  "startDate": "2024-01-01T00:00:00Z",

  "endDate": "2024-01-31T23:59:59Z"

}

This declarative approach allows business users to create a wide variety of promotions without requiring a code change. The power for developers comes from the ability to define new rules and integrate them into this system seamlessly.

Extending the Engine: Creating a Custom Offer Rule

Broadleaf’s promotions engine evaluates rules defined in the ItemCriteria domain using Spring Expression Language (SpEL). To add custom logic, a developer’s job is to make new Spring components and their methods available to the SpEL engine. The correct way to do this is by having the component implement the ExpressionVariable interface, which tells the framework how to register it in the SpEL context.

Let's use the example of a custom rule that only applies if the customer has made a purchase in the last 30 days.

Step 1: Create a Custom Service and ExpressionVariable Implementation

First, create a new Spring component that contains the method for your custom business logic. This class must implement com.broadleafcommerce.rulesengine.expression.util.ExpressionVariable to be properly registered by the rules engine.

package com.mycompany.offers.rules;

import com.broadleafcommerce.rulesengine.expression.util.ExpressionVariable;

import com.broadleafcommerce.customer.service.CustomerService;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

public class PurchaseRuleExpressionVariable implements ExpressionVariable {

    public static final String NAME = "purchaseRule";

    @Override

    public String getName() {

        return NAME;

    }

    private static final Logger LOG = LoggerFactory.getLogger(PurchaseRuleExpressionVariable.class);

    private final CustomerService customerService;

    @Autowired

    public PurchaseRuleExpressionVariable(CustomerService customerService) {

        this.customerService = customerService;

    }

    public boolean hasMadePurchaseSince(String customerId, int days) {

        if (customerId == null) {

            LOG.info("Skipping purchase check for an unauthenticated customer.");

            return false;

        }

        try {

            // Your custom logic to check for recent purchases

            // We'll assume a service method exists for this.

            return customerService.hasMadePurchaseSince(customerId, LocalDateTime.now().minusDays(days));

        } catch (Exception e) {

            LOG.error("An error occurred while checking purchase history for customerId: {}", customerId, e);

            // Default to false on error to prevent unintended promotion application

            return false;

        }

    }

}

Then override the defaultExpressionVariables bean to add the custom SpEL ExpressionVariable:

@Bean

@ConditionalOnMissingBean(name = "defaultExpressionVariables")

public Map<String, Object> defaultExpressionVariables() {

   final Map<String, Object> defaultExpressionVariables = new HashMap<>(1);

   defaultExpressionVariables

           .put(StringExpressionVariable.NAME, new StringExpressionVariable());

   defaultExpressionVariables

           .put(DateExpressionVariable.NAME, new DateExpressionVariable());

   defaultExpressionVariables

           .put(LocaleExpressionVariable.NAME, new LocaleExpressionVariable());

   defaultExpressionVariables

           .put(CollectionsExpressionVariable.NAME, new CollectionsExpressionVariable());

   defaultExpressionVariables

           .put(DecimalExpressionVariable.NAME, new DecimalExpressionVariable());

   defaultExpressionVariables

           .put(MoneyExpressionVariable.NAME, new MoneyExpressionVariable());

   defaultExpressionVariables

           .put(ObjectExpressionVariable.NAME, new ObjectExpressionVariable());

   defaultExpressionVariables

           .put(PurchaseRuleExpressionVariableService.NAME, new PurchaseRuleExpressionVariableService(customerService));

   return defaultExpressionVariables;

}

Step 2: Use the Service in a Rule String

Now, a business user can define a rule in the admin console using a SpEL string that calls your new method. The # prefix is required in SpEL to reference a bean from the context.

#purchaseRule.hasMadePurchaseSince(cart.customer.id, 30)

This is how the custom logic is seamlessly integrated into the existing, declarative rules engine. The developer writes the business logic, and the business user configures the promotions without writing any code.

Deployment and Performance Considerations

When deploying your custom rule, it's important to remember that it is a part of a Spring Boot microservice. Your custom rule code and configuration will be packaged into a new JAR file and deployed alongside or as part of the offer-service.

Deployment Best Practices

  • Service Isolation: Your custom rule should be part of a custom microservice that extends the offer-service or a custom library included in your offer-service deployment. This keeps your custom code separate from the core Broadleaf JARs.
  • Configuration Management: Use a configuration server (like Spring Cloud Config) to manage your application.yml files across different environments (dev, staging, prod). This ensures your custom rule and its properties are correctly loaded everywhere.

Performance Guidelines

  • Database Queries: The evaluation of offer rules is a frequent operation. Avoid performing heavy or un-indexed database queries within your custom rule logic. If a query is necessary, ensure it is performant and consider caching the result.
  • Caching: For logic that doesn't change often (like a customer's purchase history), consider a caching strategy (e.g., using Spring Cache with Redis). This can significantly reduce the load on your database and speed up the offer evaluation process.
  • Timeouts: If your custom rule makes calls to external services, use sensible timeouts to prevent a slow external API from degrading the performance of your entire promotions engine.

Debugging and Troubleshooting

Here are some specific scenarios and how to troubleshoot them:

  • Issue: A promotion with your custom rule isn't applying, even though you believe the conditions are met.
    • Troubleshooting: Check the logs. Broadleaf's offer-service logs the evaluation of each rule. Look for log entries related to your custom rule to see if it's being evaluated correctly and what value it's returning.
  • Issue: The custom rule is not appearing in the admin console.
    • Troubleshooting: Verify that your custom component is a valid Spring bean annotated with @Component. The framework relies on component scanning to discover your rule, so ensure it's in a package that's being scanned.
  • Issue: A dependency in your custom rule is not being injected.
    • Troubleshooting: Check your Spring application context logs for NoUniqueBeanDefinitionException or NoSuchBeanDefinitionException. This often indicates that Spring can't find or uniquely identify a dependency needed by your custom component.

Conclusion

Broadleaf's dynamic pricing and promotions engine is more than just a tool for discounts; it’s a flexible framework that empowers developers to build sophisticated, rules-based commerce logic. 

By leveraging Spring, the declarative rules system, and the power of custom extensions, you can create a platform that not only meets today's business needs but can also adapt to new strategies and market demands. The ability to give business users control over powerful, developer-defined logic is a massive competitive advantage, and Broadleaf provides the tools to make it a reality.

Related Resources