E‑commerce Pricing Engine Architecture: Promotion Calculation Using Aviator Rule Engine
This article details the design of an e‑commerce pricing engine that processes shopping‑cart items and multiple promotional activities through a three‑stage workflow, copy‑on‑trial execution, configurable Aviator rule chains, and a two‑phase discount sharing algorithm, with code examples and engineering considerations.
Background
The pricing system receives a shopping‑cart item list and a list of available activities and outputs the final per‑item amount and discount details.
Core responsibilities are divided into three stages: availability check (time/channel/product scope), discount calculation (threshold, rate, free‑order rules), and amount sharing (distribute discount to each product).
With more than 40 promotion types, coupons and gift cards, the combinatorial complexity rises sharply. The article extracts the core logic from an online system into the price-engine-core module, stripping Dubbo RPC, Nacos and keeping 32 Java classes focused on pricing.
Five‑Layer Architecture
Access Layer ComputeService — orchestration entry
Compute Engine Layer 5 Engines (Promotion / Coupon / Card / Gift / Shipping)
↑ inherits AbstractComputeEngine (template method)
Rule Engine Layer RuleFunction · FunctionRegistry · AviatorExecuteEnum (three phases)
Function Utility Layer 70+ Aviator functions + MoneyUtil / GoodsPriceShareUtil
Domain Model Layer PriceBO · ActivityBO · GoodsBO · DiscountBOThe upper layers depend on lower layers, while lower layers are unaware of the upper ones. The orchestration layer schedules engines; engines invoke rule executors that compile and run Aviator expressions, which operate on domain models.
Core Design
3.1 Copy‑on‑Trial
When multiple promotions are applied sequentially, two challenges arise: activity A may change product prices affecting activity B’s threshold, and a failure in activity B requires rolling back earlier modifications. The system adopts a Copy‑on‑Trial mode: before each activity, the product state is deep‑copied to middleGoodsList. Aviator functions operate only on the copy. If the rule chain passes, the changes are committed; otherwise the copy is discarded and the next activity starts from the original state.
protected void executeSecond() {
for (ActivityBO activity : distributeList) {
activity.setDiscountAmount(0L); // reset discount accumulation
priceBO.resetMiddleGoods(); // create trial copy
if (RuleFunction.checkAndAction(activity, priceBO)) {
priceBO.resetGoodsForMiddle(); // rule chain passed → commit
usedActivityList.add(activity);
}
// failure → copy overwritten on next reset, no explicit rollback
}
}This is essentially optimistic concurrency control applied in a single‑threaded pricing flow, similar to Git staging or MVCC snapshots.
3.2 Aviator Rule Chain
Traditional promotion systems hard‑code if‑else branches; each new promotion type requires engine code changes. This system extracts promotion logic into configurable rule‑chain strings, each activity carrying three Aviator expressions: checkExpression (AVAILABLE_CHECK) – availability validation computeExpression (CHECK_AND_ACTION) – threshold check + amount calculation showExpression (SHOW) – generate display information
Rule chains are built by concatenating function names with &&, which Aviator evaluates with short‑circuit semantics, e.g.:
"TimeCycleFunction(activity, price, executeEnum) && \
ChannelFunction(activity, price, executeEnum) && \
GoodsMatchAllPriceFunction(activity, price, executeEnum) && \
OffNCentFunction(activity, price, executeEnum)"Each function is a separate Java class extending AbstractVariadicFunction and registered via FunctionRegistry. Functions are categorized as calculation functions (OffNCent, DiscountN, FreeXNum), match functions (GoodsMatchAllPrice, TimeCycle, Channel), and display functions (ShowLabel, ShowTips).
The rule‑engine decouples rule logic from execution: adding a new promotion only requires implementing a new Function and configuring the rule chain; the engine code remains unchanged. Aviator expression compilation results are cached with compile(expr, true), eliminating repeated compile overhead.
3.3 Two‑Phase Share Algorithm
For a “full‑20‑minus‑3” coupon applied to three items (15, 10, 10), the 3 CNY discount must be allocated. Simple proportional allocation can produce zero cents for some items due to rounding. The system uses a two‑phase strategy:
public static long shareAmount(List<GoodsBO> goodsList, Long canShareAmount, ...) {
// Phase 1: guarantee allocation – each item gets at least 1 cent
long offOneCent = goodsOffOneCent(goodsList, canShareAmount, ...);
canShareAmount -= offOneCent;
// Phase 2: proportional allocation of the remainder
long scaleShare = goodsShareByScale(goodsList, canShareAmount, ...);
return scaleShare + offOneCent;
}Proportional sharing uses ROUND_UP to ensure the total allocated amount is not less than the discount, favoring platform profit while preventing any item’s share from exceeding its price.
Unified Activity Model
Promotions, coupons, and gift cards are abstracted as ActivityBO, sharing the same structure (three rule chains, metadata, discount accumulation). Differences are expressed via the ActivityEnum value. ComputeService schedules engines in a fixed order:
CouponEngine → PromotionEngine → GiftEngine → ShippingEngine → CardEngineThe order is immutable because coupons affect the amount that subsequent promotions evaluate. For example, a ¥33‑minus‑¥5 promotion only triggers if the amount after coupon deduction remains ≥ ¥33.
Each engine extends AbstractComputeEngine and follows the template method pattern:
executeFirst() // validation
executeDistribute() // conflict sorting
executeSecond() // calculation
// Subclasses override filterActivities() for activity selectionEngineering Details
Discount base is 1000 (instead of 100) to support decimal discounts with integer arithmetic, avoiding floating‑point errors.
All amount calculations use ROUND_UP to guarantee total allocated amount ≥ discount, which can accumulate bias in high‑frequency scenarios, favoring the platform.
Rule chain reset requires explicitly calling activity.setDiscountAmount(0L) before each checkAndAction to avoid leftover values.
Selection rule enums (HIGHEST, LOWEST, SECOND_HIGHEST, SECOND_LOWEST) enable expressions like “buy‑three‑get‑one‑free‑most‑expensive”.
Limitations
Copy overhead may become significant for very large orders (> 100 items). Aviator expressions lack compile‑time validation and rely on runtime testing. Execution order is hard‑coded in ComputeService; it could be further optimized with a strategy pattern or DAG scheduling.
References
Project repository: https://github.com/liuzm/price-engine-core
Technical stack: Java 11, Aviator 5.4, Lombok, Guava.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Niu Liu
A slightly rustic name 🤠 A tech veteran navigating the internet wave Hardcore tech: fixing all bugs and tough challenges
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
