SfSkills Domains Docs GitHub

Home / architect / cpq-architecture-patterns

cpq-architecture-patterns

CPQ solution architecture covering bundle design, pricing engine performance, multi-currency strategy, and integration patterns. Trigger keywords: CPQ architecture, quote scalability, bundle design, QCP performance, CPQ integration, large quote, pricing waterfall, ServiceRouter. NOT for a quote that already calculates slowly or times out — use data/cpq-performance-optimization. NOT for configuring individual Price Rules or Discount Schedules — use admin/cpq-pricing-rules.

Open the skill folder on GitHub


CPQ Architecture Patterns

This skill activates when designing the overall architecture of a Salesforce CPQ implementation — covering bundle structure, pricing engine behavior and limits, multi-currency handling strategy, QCP plugin design, and external system integration. Use it before configuration begins to establish the constraints and patterns that will govern the entire CPQ build.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

The Fixed Pricing Waterfall

CPQ calculates prices in a strict, non-configurable sequence defined by Salesforce:

  1. List Price — base price from the pricebook
  2. Contracted Price — overrides list price for a specific account if a contracted price record exists
  3. Block Price — overrides all prior prices if block pricing is configured for the product
  4. Discount Schedules — volume or term-based discounts applied after block pricing
  5. Price Rules — declarative or formula-driven price adjustments
  6. Net Price — final calculated price visible on the quote line

This sequence is fixed. Architects cannot reorder stages. Any customization that attempts to modify a price "before" a later stage has already run will produce incorrect results. Price Rules, despite their power, always run after Discount Schedules — architects who design multi-stage discount logic must account for this ordering.

Quote Line Editor (QLE) Line Item Limits

The QLE is a Lightning component that renders all quote lines in the browser and performs synchronous calculations. At 200–300 line items, quote save operations become unreliable due to Apex CPU time limits, governor limits on SOQL/DML, and browser payload size. This is not a configurable limit — it is a consequence of how the QLE executes the pricing engine synchronously.

Architectural responses to this constraint:

Quote Calculator Plugin (QCP) Architecture

The QCP is a JavaScript escape hatch that allows custom pricing logic to run inside the CPQ pricing engine. It is stored in the SBQQ__Code__c field on the SBQQ__CustomScript__c object. That field has a hard cap of 131,072 characters. For any non-trivial plugin:

QCP functions run asynchronously; all pricing callbacks must return Promises. Blocking patterns or synchronous-style code will cause silent failures.

Multi-Currency Storage and Limitations

When multi-currency is enabled in a CPQ org, all CPQ price fields (list price, unit price, net price) are stored in the org's corporate currency. Currency conversion to the quote's transaction currency happens at display time using the static exchange rate configured in the org.

CPQ does not natively support dated exchange rates. If a customer requires that prices use the exchange rate in effect at a specific date (e.g., contract signature date), there is no out-of-box mechanism. Documented workarounds include:

Architects must surface this limitation explicitly when multi-currency and financial accuracy requirements coexist.


Common Patterns

Pattern: Flat Bundle vs. Nested Bundle Design

When to use flat bundles: Single-level product groupings where all components belong to the same parent, no sub-grouping required, and QLE performance is a concern. Flat bundles (1 parent, N components with SBQQ__FeatureName__c grouping) are simpler to maintain, easier to price, and have lower QLE rendering cost.

When to use nested bundles: When the product genuinely has distinct tiers — e.g., a base platform product with sub-bundle hardware options and a separate sub-bundle for software licenses. Nested bundles increase quote line count and QLE complexity. Each additional nesting level multiplies the SOQL calls during calculation.

Constraint: Deeply nested bundles (3+ levels) frequently cause pricing engine timeouts. Prefer flat bundles with Option Constraints to simulate grouping behavior without additional nesting.

Option Constraints (SBQQ__OptionConstraint__c) enforce conditional logic between product options — use them instead of nested bundles when the goal is conditional inclusion rather than genuine product hierarchy.

Pattern: Static Resource QCP for Complex Plugins

When to use: Whenever QCP JavaScript exceeds approximately 80,000 characters (safe margin below 131,072), or when the plugin requires unit-testable modular code.

How it works:

  1. Write the full plugin JavaScript in a Static Resource (e.g., CPQCalculatorPlugin).
  2. In SBQQ__Code__c, deploy a loader:
(function() {
  var resourceUrl = '/resource/CPQCalculatorPlugin';
  var xhr = new XMLHttpRequest();
  xhr.open('GET', resourceUrl, false); // synchronous load at init time
  xhr.send(null);
  if (xhr.status === 200) {
    eval(xhr.responseText);
  }
})();
  1. The actual plugin callbacks (onBeforeCalculate, onPriceRules, etc.) are defined in the Static Resource.
  2. Deploy via StaticResource metadata type — no manual copy-paste into the org field.

Why not inline: Field character limit will silently truncate code, causing the plugin to fail with cryptic errors. The truncation point is not surfaced in validation.

Pattern: External System Integration via ServiceRouter

When to use: Any time an external system (ERP, billing platform, subscription management) needs to create, update, or read CPQ quotes, products, or calculate pricing.

How it works: Salesforce CPQ exposes a REST API at /services/apexrest/SBQQ/ServiceRouter. External systems call this endpoint with a JSON payload specifying the reader or saver class (e.g., SBQQ.QuoteService.read / SBQQ.QuoteService.save).

Why not direct DML: Writing directly to SBQQ__Quote__c, SBQQ__QuoteLine__c, or related objects via REST/SOAP API or integration user DML completely bypasses the pricing engine. Prices will not be recalculated, required fields may not be populated by triggers, and the quote will be in a corrupt state. This is the single most common architectural mistake in CPQ integrations.


Decision Guidance

SituationRecommended ApproachReason
Quote expected to exceed 200 line itemsEnable Large Quote Mode; communicate async UX to stakeholdersQLE synchronous calculation limit; save operations become unreliable above 200-300 lines
QCP logic exceeds ~80K charactersStore in Static Resource, use loader pattern in SBQQ__Code__cField hard cap of 131,072 chars; truncation causes silent failures
External ERP needs to create/update quotesUse ServiceRouter REST API exclusivelyDirect DML bypasses pricing engine; results in corrupt quote data
Multi-currency with dated exchange rates requiredCustom pricebook-per-currency or custom Apex rate table; escalate as gapCPQ does not natively support dated exchange rates
Product hierarchy with 3+ nesting levelsFlatten to 2 levels + Option ConstraintsDeep nesting multiplies SOQL per calculation; frequent timeout risk
Bundle with conditional component selectionUse Option Constraints on flat bundleSimpler, fewer lines, less QLE load than nested sub-bundles
Large Quote Mode adoptionPilot with internal users first; set SBQQ__LargeQuote__c = true on AccountUX change is significant — no real-time price feedback; requires user training

Step-by-step instructions for an AI agent or practitioner working on CPQ architecture:

  1. Gather constraints: Collect maximum quote line count, multi-currency requirements, external system integration requirements, and estimated QCP complexity. These four inputs drive the most consequential architectural decisions.
  2. Assess QLE line limit risk: If any quote scenario may produce more than 150 line items, plan for Large Quote Mode from day one. Design bundle structures to minimize visible line count — set sub-components to hidden where users do not need to interact with them.
  3. Design bundle structure: Choose flat vs. nested bundle strategy. Document the maximum nesting depth. Default to flat with Option Constraints unless the product model requires genuine hierarchy. Validate the design against QLE rendering cost before configuration begins.
  4. Plan QCP architecture: Determine whether a QCP is needed. If yes, estimate JavaScript size. If likely to exceed 80,000 characters, plan the Static Resource loader pattern from the start — retrofitting this pattern after QCP is live requires a deployment freeze.
  5. Define multi-currency strategy: If multi-currency is enabled, document whether dated exchange rates are a business requirement. If yes, design the workaround (pricebook-per-currency or custom Apex rate table) and surface it as a scope item — this is not a configuration-only solution.
  6. Define integration touchpoints: For each external system, identify whether it reads or writes CPQ data. Mandate ServiceRouter for all write operations. Provide the external system team with the ServiceRouter API documentation and confirm they will not use direct DML.
  7. Validate and review: Run scripts/check_cpq_architecture.py against the metadata directory. Review checklist items. Confirm Large Quote Mode decision is documented and communicated to project stakeholders if applicable.

Review Checklist

Run through these before marking CPQ architecture work complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. QCP field truncation is silent — If SBQQ__Code__c content exceeds 131,072 characters, Salesforce truncates it without error. The plugin appears to save successfully but executes truncated/broken JavaScript. The failure manifests as incorrect pricing or no pricing, with no clear error message pointing to truncation.
  2. Direct DML on SBQQ objects corrupts quotes — Writing to CPQ objects via external API (REST, SOAP, integration user) bypasses all CPQ triggers and the pricing engine. Quote lines will show stale or zero prices, required calculated fields will be empty, and the quote cannot be reliably activated. There is no automated warning when this happens.
  3. Discount Schedules always run before Price Rules — The waterfall sequence is fixed. A Price Rule that sets a price field will run after Discount Schedules have already applied. Architects who expect Price Rules to set a "base" price before volume discounts apply will get incorrect results. Design Price Rules to adjust net price, not reset it.
  4. Large Quote Mode disables real-time pricing feedback — When SBQQ__LargeQuote__c is set on an Account or enabled globally, the QLE no longer recalculates prices as users edit lines. Users must trigger calculation manually. This is a significant UX regression that must be communicated and trained on before go-live.
  5. Multi-currency exchange rate is static at display time — CPQ converts corporate-currency prices to transaction currency using the org's current exchange rate. If the org exchange rate changes after a quote is created, the displayed price changes retroactively. There is no snapshot of the rate at quote creation time unless custom code captures it.

Output Artifacts

ArtifactDescription
CPQ Architecture Decision RecordDocuments bundle strategy, QLE line limit decisions, QCP architecture, multi-currency approach, and integration patterns
Bundle Design DiagramVisual showing parent/component/option structure, nesting depth, and hidden component strategy
QCP Code Architecture PlanSpecifies inline vs. Static Resource approach, loader pattern if applicable, and deployment strategy
Integration Pattern SpecificationLists all external system touchpoints and confirms ServiceRouter usage for write operations
Large Quote Mode Enablement PlanStakeholder communication plan, pilot scope, and SBQQ__LargeQuote__c field configuration approach

Package files