Build
What to build.
CEL evaluation architecture, the PromotionEvaluationContext struct,
two-repo delta, and phased delivery model. Engineering audience.
The structural gap
Email is not in the Go service domain struct. It is not in ExecutePromotionRequest.
The Sony use case is structurally impossible today.
type Customer struct {
ID CustomerID // uint32
GroupID GroupID // uint32
}
// Email is not here. It is not passed in ExecutePromotionRequest.
// evaluateCustomerGroup() checks GroupID only.
The RPC serializer maps customerProto.GetId() and customerProto.GetGroupId()
— nothing else. Billing email is in the cart record at checkout step 4 but is not extracted into the promotion context.
Source: bc-promotions-engine.md ↗
Recommended architecture: CEL + typed context
Option C from the architectural options analysis.
CEL (Common Expression Language) is non-Turing-complete, Go-native, and production-proven
in Kubernetes admission webhooks. The PromotionEvaluationContext struct
is the central abstraction — assemble it once per checkout call, evaluate all conditions against it.
Source: architectural-options.md ↗
type PromotionEvaluationContext struct {
Customer struct {
ID CustomerID
GroupID GroupID
Email string // Phase 1 — free from billing_address in cart record
EmailDomain string // Phase 1 — derived from Email at assembly time
Company string // Phase 2 — one Redis-cached gRPC call (~2–5ms)
}
Cart CartContext
Channel ChannelContext
}
type CartContext struct {
Subtotal float64
ItemCount int
CouponCodes []string
}
type ChannelContext struct {
ID int
CurrencyCode string
} CEL evaluation flow
The context assembly step is the critical path surface. All conditions test against the assembled struct; the CEL env knows nothing about "email domain" — it evaluates typed predicates over the context object.
Two-repo delta
- +
CustomerEmailDomainCondition— new typed condition class in the conditions DSL - ~
RequestToContextTransformer— extractbilling_address.emailinto the request - +
promotions-managermicroapp — register "Email domain" condition type in the CP rule builder UI - + Schema for new condition type parameters (operator enum + value string)
- ~
internal/domain/customer.go— addEmail stringfield - ~ RPC serializer (
get_eligible_featured_promotions_serializer.go) — mapbilling_address.emailfrom proto - +
internal/expression/— new package: CEL env setup +ExpressionConditiontype - ~
go.mod— addgithub.com/google/cel-go
Why the Go service is the right home for the CEL model: the PHP monolith's typed condition DSL handles Phase 1. The Go service extraction is where CEL expression evaluation belongs long-term — design it correctly now while the struct is a 2-field stub, not after it has 20 callers. Source: architectural-options.md ↗
Phased delivery
Email domain condition (PHP)
- +
CustomerEmailDomainConditionin the typed DSL - ~
RequestToContextTransformermapsbilling_address.email - + CP rule builder UI registers the new condition type
Zero additional service calls. Email is already in the cart record at step 4.
CEL + Go service extraction
- +
internal/expression/— CEL env +ExpressionCondition - +
PromotionEvaluationContextreplaces baredomain.Customer - ~ Go service extraction reaches customer-attribute evaluation path
Design the CEL model correctly now — the Go struct is a 2-field stub with zero callers yet.
Custom attributes via customers-scala
- + Customer materialized-view snapshot (Redis, domain events)
- + gRPC call to
customers-scalafor custom form fields - + Company name, job role, and arbitrary custom attribute keys
Latency budget: 5–15ms gRPC call amortized via snapshot. Async refresh on customer.updated events.
Guest checkout email is already paid for
billing_address.email is in the cart record at checkout step 4
for all customers — guest and authenticated alike. No new service call.
Shopify's email domain targeting requires sign-in; guest customers are excluded.
This is a real differentiation point for B2B use cases where buyers check out as guests
but enter corporate email addresses.
Decision lineage
Architectural options analysis
Why CEL over OPA. Why Phase 1 uses PHP typed conditions. Why the Go service is the right home for the expression engine.
→ research/current-state/architectural-options.md
BC promotions engine — current state
Full execution flow, DiscountContext structure, feature flag gating for the Advanced Promotion Manager.
→ research/current-state/bc-promotions-engine.md