Designing an Efficient Rule Builder for Shopware 6: Requirements, Minimal Architecture, and Operational Safeguards
Learn how to architect Shopware’s Rule Builder for low‑latency cart and promotion logic, with a lightweight rule‑evaluation engine, clear trust boundaries, and operational checks to catch failures early.
11 Jan 2026, 14:50 UTC

Problem Statement
Shopware’s Rule Builder lets merchants define promotional, shipping, and pricing rules without code. The challenge is to evaluate these rules on every request with sub‑millisecond latency, while keeping the system stateless, secure, and easy to test. This note outlines the minimal design that satisfies those constraints, the trust boundaries that must be respected, the operational checks needed to keep the engine healthy, and the failure modes that would trigger a redesign.
Requirements
- Conditions must be evaluable per HTTP request (e.g., cart total, customer group, order date).
- Evaluation time should stay below a few milliseconds to avoid impacting storefront latency.
- Rule definitions live in the database but are immutable during a request; no runtime schema changes.
- Rule engine must be stateless and safe to run in a shared PHP process.
- External data (third‑party APIs) can be used but must be fetched before rule evaluation and treated as untrusted.
Smallest Suitable Design
The core of the engine is a RuleEvaluator service that receives a RuleContext object and returns a boolean. The context contains the cart, customer, session, and any pre‑fetched external data. Rules are stored as a JSON tree in the database; each node is either a logical operator (AND/OR) or a leaf condition implemented by a PHP callback.
Data Structures
// Rule definition stored in the database
$rule = [
"id" => "promo_10vip",
"name" => "10% off for VIPs over 200",
"conditions" => [
["type" => "and", "children" => [
["type" => "customer_group", "value" => "VIP"],
["type" => "cart_total", "operator" => ">=", "value" => 200]
]]
]
];
Evaluation Flow
- The storefront controller builds a
RuleContextfrom request data. - The
RuleEvaluatortraverses the condition tree recursively.- Logical nodes combine child results with
andoror. - Leaf nodes call a registered
ConditionInterfaceimplementation.
- Logical nodes combine child results with
- The evaluator returns
trueorfalseto the controller.
Condition Interface
interface ConditionInterface
{
public function evaluate(RuleContext $context, array $parameters): bool;
}
Example CustomerGroupCondition:
class CustomerGroupCondition implements ConditionInterface
{
public function evaluate(RuleContext $ctx, array $params): bool
{
return in_array($params["value"], $ctx->getCustomerGroups(), true);
}
}
Trust/Data Boundaries
- The evaluator runs in the same PHP process as the storefront controller, so it inherits the same trust level. All input comes from validated request data, the cart object, and the logged‑in customer.
- Any external API calls must be performed *before* the evaluation step. The fetched data is stored in the context as plain values, not as executable code.
- Rule definitions are read‑only during a request; they are loaded once per request from the database. No mutation of the rule tree occurs during evaluation.
Operational Checks
- Unit tests for each condition type – ensure the callback logic is correct for all supported operators.
- Integration test – load a real rule set from the database, execute it against a known cart and customer, and assert the expected boolean result.
- Profiler hook – register a listener on the
RuleEvaluator::evaluatemethod to record execution time. If any rule exceeds 5 ms, alert the operations team. - **Database migrations** – store rule definitions in a dedicated table with a
migration_versioncolumn. Runphp bin/console doctrine:migrations:statusto verify all migrations are applied.
Example Command to Verify Migration Status
# Run from the Shopware root directory
php bin/console doctrine:migrations:status
# Expected output:
# Current: 20241001120000
# Latest: 20241001120000
# Executed: 20241001120000
Failure Modes & Design‑Change Triggers
- Exception in a condition – the
RuleEvaluatorcatches all exceptions, logs the error, and returnsfalse(fail‑safe). This prevents a single buggy rule from breaking the entire checkout flow. - Slow rule evaluation – if the profiler reports >5 ms for a rule, investigate the condition logic. A common culprit is a database call inside a condition, which violates the stateless assumption.
- Need for external data during evaluation – if a rule must call a third‑party service synchronously (e.g., real‑time currency conversion), the current design must be replaced with a cached pre‑evaluation layer or an async job that enriches the cart before evaluation.
- Complex data joins – when a rule requires joining multiple tables (e.g., loyalty points from a separate system), the engine should shift to a cached rule‑engine that pulls the necessary data once per request.
- Rule definition changes in production – manual edits to the rule table bypass migrations and can cause drift. Enforce migration‑only changes and run the integration test suite after every deployment.
Practical Verification Checklist
- Deploy the rule‑builder code to staging.
- Create a test rule in the database:
- Rule ID:
promo_test_10vip - Condition: Customer group
VIPAND cart total >= 200
- Rule ID:
- Log in as a user in group
VIP, add items totaling 250 to the cart, and navigate to the checkout page. The promotion should appear. - Switch to a user outside
VIPand repeat; the promotion should be absent. - Check the Shopware profiler for the rule evaluation time. It should be <5 ms.
- Run
php bin/console doctrine:migrations:statusto confirm migrations are up to date. - Run
vendor/bin/phpunit --testsuite rule-builderto ensure all unit and integration tests pass.
Limitations
- All conditions must be pure functions that do not modify global state.
- The engine cannot perform blocking I/O; any external dependency must be resolved before evaluation.
- Large rule trees (hundreds of nodes) can still push latency; consider caching or flattening complex conditions.
Conclusion
By keeping the rule evaluator stateless, evaluating a lightweight JSON tree, and enforcing strict trust boundaries, Shopware can deliver near‑instant rule decisions for promotions and shipping. Operational checks such as unit tests, integration tests, and profiler alerts give confidence that the engine remains performant. When the rule set grows to require external calls or complex joins, the design should shift to a cached or asynchronous pre‑evaluation layer to preserve latency guarantees.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.