Designing a Marketing PMS: Activity Model, Rule Engine, Conflict Handling, and Allocation Mechanisms

The article explains the core responsibilities and three‑layer architecture of a marketing PMS, details the activity‑template model, walks through the rule‑engine execution flow with container‑component and needNum logic, compares self‑built and Drools solutions, and describes mutual‑exclusion, stacking, allocation strategies, and the three‑stage cal/use/rollback workflow with caching and consistency safeguards.

samdeepthink
samdeepthink
samdeepthink
Designing a Marketing PMS: Activity Model, Rule Engine, Conflict Handling, and Allocation Mechanisms

PMS Core Responsibilities

The PMS (Promotion Management System) provides the execution engine for marketing strategies, handling template definition, activity creation, product and store binding, lifecycle control, and mutual‑exclusion validation. It also performs discount calculation and allocation, and coordinates marketing assets such as coupons and member benefits.

Activity Model Design

Activities are split into two layers: Template (the skeleton of a promotion, e.g., "Buy N items, get the most expensive one free") and Activity (a concrete instance of a template with specific products, stores, and time windows). This separation avoids duplicated rule logic when many similar activities are created.

Product and Store Binding

Product range supports a "All Products" shortcut flag.

Store range supports a similar "All Stores" flag.

Bindings are stored in a separate association table; the engine first filters activities by store, then evaluates each activity's rules.

Rule Engine Architecture

The engine receives an order’s product list and the list of applicable activities for the current store. It processes each activity sequentially, executing its rule chain.

Execution Flow for a Single Activity

DataConverter : extracts required input parameters from the rule configuration and order data.

RuleActuator : runs the specific judgment or calculation logic.

After : writes the result back to the execution context for downstream rules.

This three‑step design allows new rules to be added by implementing only these methods.

Container‑Component Model

Activities contain Containers , each grouping a set of Components (individual rule instances). Containers enable OR logic between components. The needNum attribute controls the logical semantics: needNum == total components: AND (all must satisfy). needNum == 1: OR (any one satisfies). needNum == N: at least N components must satisfy.

Operators can be changed in the backend without code changes.

Rule Classification and Interruption Mechanism

Rules are divided into four types, ordered by priority:

Extent : checks hard pre‑conditions (product/store range, time). Failure aborts the entire activity.

Threshold : checks soft conditions (e.g., minimum quantity). Failure marks the activity as "not participating" but does not abort the engine.

Preferential : computes the actual discount amount. Failure yields zero discount without aborting.

Show : generates front‑end display text (e.g., "Buy one more to get discount"). Does not affect calculation results.

The design treats extent rules as hard constraints to save computation and guarantee deterministic results, while threshold rules allow the engine to continue for user‑facing prompts.

Concrete Rule Implementations

Product Matching Rule : reads the configured product IDs; if "All Products" is set, all order items are added to the participation list. If the list is empty, the activity is aborted.

Buy N Items, Get Most Expensive Free : counts participating items; if the count ≥ N, sorts them by price descending and selects the highest‑priced item as the discount amount, adding that item to the allocation list.

Spend N Yuan, Reduce M Yuan : sums the total amount of participating items; if the sum ≥ N, the discount equals M (capped by the total amount). Allocation is set to "whole order" because the discount is amount‑based.

Purchase‑Limit Rule : reads the user’s purchase count from Redis; if the count ≥ limit, the activity is marked "not participating". Redis atomic INCR ensures concurrency safety, with a DB fallback for Redis failures.

Self‑Built Engine vs. Drools vs. EasyRules

Because marketing rules are individually simple but combinatorial, a custom container‑component model with needNum control and type‑based interruption is more expressive than Drools, which excels at complex inference. EasyRules supports simple rule combinations but lacks native AND/OR control and fine‑grained interruption.

Complexity Fit : Self‑built – simple rules, many combinations; Drools – complex inter‑rule dependencies; EasyRules – simple rules, simple combinations.

AND/OR Control : Self‑built – native via needNum; Drools – requires agenda‑group + activation‑group; EasyRules – custom implementation needed.

Interrupt Granularity : Self‑built – rule‑type based; Drools – salience + no‑loop (less intuitive); EasyRules – not supported.

Performance : Self‑built – direct Java calls; Drools – rete network overhead; EasyRules – reflection overhead.

Rule Hot‑Update : Self‑built – custom DB + cache solution; Drools – native DRL hot‑load; EasyRules – custom solution.

Learning Cost : Self‑built – container‑component model; Drools – DRL syntax, rete algorithm; EasyRules – low.

Maintenance Cost : Self‑built – new rule types need Java code; Drools – new DRL file; EasyRules – new Java code.

Activity Mutual Exclusion and Stacking

Mutual exclusion is configured at the template level, not the activity level. Activities under the same template are mutually exclusive; activities from different templates can stack.

Exclusion dimensions:

Store‑level: same store cannot have overlapping activities of the same template.

Product‑level: same product cannot appear in multiple activities of the same template.

Dual‑level: both store and product must not overlap.

Exclusion checks occur during activity creation/modification to avoid runtime performance penalties and to guarantee deterministic front‑end display.

When stacking, the engine processes activities in the order returned by the activity list, applying each discount to the remaining amount of the product, ensuring the total discount never exceeds the original price.

Allocation Mechanism

Three scenarios drive the need for allocation:

Partial refunds – need to know how much discount each item received.

Financial reconciliation – per‑item revenue accounting.

Profit sharing – precise per‑item amounts for multi‑party settlements.

Two allocation dimensions:

Item‑level : discount is distributed proportionally to each item's amount.

Order‑level : discount applies to the whole order (e.g., free shipping).

Allocation algorithm for item‑level:

// Calculate each item's allocated amount
itemAllocation = itemRemainingAmount / totalRemainingAmount * totalDiscount

Because of rounding, the sum may differ by a cent. The fix is to compute the last item's allocation as totalDiscount – sum(previous allocations) , guaranteeing the total matches exactly.

Three‑Stage Workflow and Consistency Guarantees

The PMS exposes four core APIs: cal (calculate), use (confirm), rollback (revert), and share (query allocation). These cover the order lifecycle from browsing to refund.

cal Stage – Discount Calculation

Triggered when the user opens the order confirmation page. Two scenarios:

Browse: calculate only, no caching.

Place Order: calculate and cache the result in Redis with a 15‑minute TTL for later idempotent confirmation.

use Stage – Confirmation

After payment, the system reads the cached result, persists discount records, and decrements purchase‑limit counters. Idempotency is ensured by a unique (user + order) constraint; repeated calls do not duplicate effects.

rollback Stage – Refund

When a refund occurs, the API increments the purchase‑limit counter (Redis DECR ) and marks the discount record as rolled back. A unique rollback identifier prevents duplicate rollbacks.

Activity Change Detection

During the cal phase, the PMS generates an MD5 digest of the activity data and returns it with the result. The use call includes this digest; the PMS recomputes the digest and compares it. If they differ, the activity was modified after the user saw the promotion, and the system aborts with an error, forcing the user to reconfirm.

Two‑Level Cache

Marketing data is cached locally in the JVM (fastest, limited size) and in Redis (second‑level). On updates, a version number triggers invalidation; the system refreshes the local cache when version mismatches are detected.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendJavarule enginemarketingallocationdiscount
samdeepthink
Written by

samdeepthink

Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.