Strategy → Architectural fork

Typed conditions vs. expression engine.

Four architectural options for adding customer-attribute targeting to BC's promotions engine. The recommendation is a two-track approach: Option A in the PHP monolith now to ship the Sony use case, Option C (CEL) in the Go service extraction in parallel because the struct is a 2-field stub with zero callers — the right moment to design it correctly.

Source authority: research/current-state/architectural-options.md ↗

Option A — Phase 1 recommended

Typed PHP conditions

Add CustomerEmailDomainCondition to the existing typed DSL in the PHP monolith. One transformer change, one new condition class, one CP rule builder registration. Proof of concept for the transformer plumbing. Ships the Sony use case immediately. Zero new service calls — email is already in the checkout record.

Ships the Sony use case. Minimal scope. No new dependencies.

Option C — Phase 2 recommended

CEL expression engine (Go)

Add github.com/google/cel-go to the Go promotions service. Replace the bare domain.Customer struct with a typed PromotionEvaluationContext. Compile-once-eval-many, non-Turing-complete sandbox. The Go service currently has 2 fields and zero callers at the evaluation layer — design it correctly now, not after 20 callers exist.

Unlocks general extensibility. Right architectural moment. Medium scope.

All four options

Full comparison from architectural-options.md. Options B and D are assessed but not recommended.

Option Label Where Extensibility New condition cost Platform risk Status
A Typed PHP conditions (DSL extension) Phase 1 bigcommerce/bigcommerce (PHP monolith) Typed per-field New class per condition type (EmailDomainCondition, CompanyCondition, …) low ✓ recommended
B Attribute map / dynamic key-value bigcommerce/bigcommerce (PHP monolith) Typed per-field New key registered in attribute map schema low not recommended
C CEL expression engine Phase 2 bigcommerce/promotions (Go service) General purpose No new code — merchants write expressions over the context struct medium ✓ recommended
D Eligibility functions (Shopify model) Merchant-authored code (Functions API) General purpose Merchant writes and deploys a function high not recommended

Why not Option B (attribute map)?

An attribute map still requires a new entry per condition type — the extensibility gain over Option A is marginal. The trade-off is reduced type safety and less discoverable CP authoring (merchants see a generic key picker instead of named condition types). The structured condition DSL in Option A is both safer and more merchant-friendly.

Why not Option D (eligibility functions)?

The Shopify Functions model pushes implementation complexity to merchants — every new condition type requires the merchant to write and deploy code. This breaks the no-code CP authoring surface that is the primary merchant value proposition for Advanced Promotions. Ruled out: merchant sophistication requirement and loss of the no-code path.

The context assembly problem

The expression engine evaluates against a typed context struct. The cost of that struct depends entirely on where the data comes from — not on the evaluator itself.

Zero cost — already in flight

cart.email

The checkout-entry email is in the cart record for all customers — guest and authenticated alike — by the time promotions evaluate. The data exists; it is simply not extracted into ExecutePromotionRequest today. No new service call. This is Phase 1.

~2–5ms — Redis-cached gRPC

Company name, customer record fields

Available via a gRPC call to the customer record — Redis-cached, ~2–5ms on cache miss. Requires a customer materialized-view snapshot keyed by customer ID. Phase 2.

~5–15ms — gRPC + async snapshot

Custom form fields

Require a gRPC call to customers-scala. Best served via an async materialized-view snapshot refreshed on customer.updated events. Requires customers-domain coordination. Phase 3.

Source: bc-customer-data-model-and-feasibility.md ↗ §3, architectural-options.md ↗ §5

Source documents