All Skills
A
sfb2b-apex
Platform APIs & Operations
Apex development patterns and best practices for Salesforce B2B Commerce. Triggers on: Apex, controller, service, trigger, batch, test class, SOQL, DML, Salesforce backend, server-side code
Asfb2b-apex
$
npx skills add Architect-And-Bot/sf-b2b-commerce-template --skill sfb2b-apexSalesforce B2B Commerce Apex Development
Architecture Pattern: Service-Selector-Domain (SSD)
The SSD pattern enforces strict separation of concerns across the Apex layer:
- Services: Contain all business logic, orchestration, and domain coordination. Services call Selectors for queries and Domains for mutations.
- Selectors: Handle all SOQL queries exclusively. No business logic. All queries must be in Selector classes.
- Domains: Handle DML operations (insert, update, delete, upsert) and trigger logic. Maintain data consistency and enforce domain rules.
- Controllers: LWC integration layer only. Use @AuraEnabled methods. Delegate to Services immediately.
Every Apex class must follow this separation. Do not mix query, mutation, and business logic in a single class.
Naming Conventions
Services
- Pattern:
{{projectPrefix}}_[Feature]Service.cls - Examples:
{{projectPrefix}}_CartService.cls,{{projectPrefix}}_OrderSyncService.cls,{{projectPrefix}}_PricebookService.cls - Contains:
public class {{projectPrefix}}_[Feature]Service { public void execute() {} }
Selectors
- Pattern:
{{projectPrefix}}_[Object]Selector.cls - Examples:
{{projectPrefix}}_ProductSelector.cls,{{projectPrefix}}_CartSelector.cls,{{projectPrefix}}_OrderSelector.cls - Contains: All SOQL queries for the object
- Method naming:
selectById(Set<Id> ids),selectByParentId(Set<Id> parentIds),selectAll()
Controllers
- Pattern:
{{projectPrefix}}_[Feature]Controller.cls - Examples:
{{projectPrefix}}_CartController.cls,{{projectPrefix}}_CheckoutController.cls - Contains: @AuraEnabled methods only
- Pattern: Immediate delegation to Services
Triggers
- Pattern:
{{projectPrefix}}_[Object]Trigger.trigger - Examples:
{{projectPrefix}}_Order__cTrigger.trigger,{{projectPrefix}}_Cart_Item__cTrigger.trigger - Contains: Only handler delegation (old-school logic disabled)
- Handler class pattern:
{{projectPrefix}}_[Object]TriggerHandler.cls
Test Classes
- Pattern:
{{projectPrefix}}_[Class]Test.cls - Examples:
{{projectPrefix}}_CartServiceTest.cls,{{projectPrefix}}_ProductSelectorTest.cls - Minimum coverage: 85%
B2B Commerce Apex APIs
ConnectApi Classes
- ConnectApi.CommerceCart: Cart creation, item management, checkout
- ConnectApi.CommerceCatalog: Product catalog browsing, pricing
- ConnectApi.CommerceOrder: Order history and details
Cart Extensions
Cart extensions hook into the commerce engine for custom behavior:
- Pricing Extension (
ConnectApi.CartItemPriceExtension)- Override product pricing with custom logic
- Access discount matrices, contract pricing, tiered volumes
- Called before cart calculation
- Shipping Extension (
ConnectApi.CartShippingMethodExtension)- Calculate shipping costs dynamically
- Validate shipping methods for address
- Integrate with shipping provider APIs
- Tax Extension (
ConnectApi.CartItemTaxExtension)- Integrate with tax provider for tax calculation
- Handle exemption certificates
- Split tax across product and shipping
- Inventory Extension (
ConnectApi.CartItemInventoryExtension)- Check real-time inventory (ERP)
- Reserve inventory on checkout
- Warn on low stock
CartItemValidator
Custom validation framework for cart operations:
public interface CartItemValidator {
void validateCartItem(ConnectApi.CartItem cartItem, List<ConnectApi.CartValidationError> errors);
}
CheckoutSessionAction
Extend checkout with custom business logic:
- Pre-checkout validation
- Payment method selection
- Order creation coordination
Integration Patterns
HttpCalloutService Base Class
All HTTP integrations inherit from a base service:
public abstract class HttpCalloutService {
protected String endpoint;
protected String method = 'POST';
protected Integer maxRetries = 3;
protected Integer retryDelayMs = 1000;
public HttpResponse execute(String body) {
return executeWithRetry(body, 0);
}
protected virtual HttpResponse executeWithRetry(String body, Integer attempt) {
// Exponential backoff: 1s, 2s, 4s, 8s
// Circuit breaker logic
}
}
Named Credentials
Always use Named Credentials for endpoint configuration:
- No hardcoded URLs in code
- Centralized secret management
- OAuth/Certificate auth handled by SF
Retry with Exponential Backoff
// Retry delays: 1000ms, 2000ms, 4000ms
// Formula: baseDelay * Math.pow(2, attempt - 1)
// Max retries: 3
// Only retry on transient errors (5xx, timeout)
Circuit Breaker Pattern
Prevent cascading failures when external system is down:
- Open: Block all calls, return cached/default response
- Closed: Allow calls, track failures
- Half-open: Allow single test call to determine recovery
- Threshold: 5 consecutive failures -> Open
Platform Events
Publish domain events for asynchronous processing:
{{projectPrefix}}_OrderSyncEvent__e-> Trigger order sync to ERP{{projectPrefix}}_InventoryUpdateEvent__e-> Update stock levels{{projectPrefix}}_ErrorEvent__e-> Error tracking and alerting
Code Templates
Service Class Template
public with sharing class {{projectPrefix}}_CartService {
private {{projectPrefix}}_CartSelector cartSelector;
public {{projectPrefix}}_CartService() {
this.cartSelector = new {{projectPrefix}}_CartSelector();
}
public CartDTO getCart(String cartId) {
List<Order> carts = cartSelector.selectById(new Set<Id>{(Id)cartId});
if (carts.isEmpty()) {
throw new {{projectPrefix}}_ValidationException('Cart not found: ' + cartId);
}
return new CartDTO(carts[0]);
}
public void addItem(String cartId, String productId, Integer quantity) {
// Validation
if (quantity < 1) {
throw new {{projectPrefix}}_ValidationException('Quantity must be > 0');
}
// Delegate to domain for DML
{{projectPrefix}}_CartDomain cartDomain = new {{projectPrefix}}_CartDomain();
cartDomain.addItem(cartId, productId, quantity);
}
}
Selector Class Template
public with sharing class {{projectPrefix}}_CartSelector {
public List<Order> selectById(Set<Id> cartIds) {
return [
SELECT Id, Name, TotalAmount, Status, OwnerId
FROM Order
WHERE Id IN :cartIds
ORDER BY CreatedDate DESC
];
}
public List<Order> selectByParentId(Set<Id> accountIds) {
return [
SELECT Id, Name, TotalAmount, Status, AccountId
FROM Order
WHERE AccountId IN :accountIds
ORDER BY CreatedDate DESC
];
}
public List<Order> selectAll() {
return [SELECT Id, Name FROM Order LIMIT 10000];
}
}
LWC Controller Template
public with sharing class {{projectPrefix}}_CartController {
@AuraEnabled(cacheable=true)
public static CartDTO getCart(String cartId) {
try {
{{projectPrefix}}_CartService service = new {{projectPrefix}}_CartService();
return service.getCart(cartId);
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
@AuraEnabled
public static void addToCart(String cartId, String productId, Integer quantity) {
try {
{{projectPrefix}}_CartService service = new {{projectPrefix}}_CartService();
service.addItem(cartId, productId, quantity);
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
}
Cart Extension (Pricing Calculator)
public class {{projectPrefix}}_PricingExtension implements ConnectApi.CartItemPriceExtension {
public void extendCart(ConnectApi.CartExtensionInput input) {
// Access cart items
List<ConnectApi.CartItem> cartItems = input.getCartItems();
for (ConnectApi.CartItem item : cartItems) {
Decimal customPrice = calculatePrice(item.getProductId(), item.getQuantity());
item.setPrice(customPrice);
}
}
private Decimal calculatePrice(String productId, Integer quantity) {
// Apply contract pricing, volume discounts, customer-specific rates
{{projectPrefix}}_PricebookService service = new {{projectPrefix}}_PricebookService();
return service.getPriceForProduct(productId, quantity, UserInfo.getUserId());
}
}
Trigger Handler Template
public class {{projectPrefix}}_OrderTriggerHandler {
public static void handle(List<Order> newRecords, List<Order> oldRecords,
Map<Id, Order> newMap, Map<Id, Order> oldMap,
Boolean isInsert, Boolean isUpdate, Boolean isDelete) {
if (isInsert) {
onBeforeInsert(newRecords);
} else if (isUpdate) {
onBeforeUpdate(newRecords, oldMap);
} else if (isDelete) {
onBeforeDelete(oldRecords);
}
}
private static void onBeforeInsert(List<Order> records) {
{{projectPrefix}}_OrderDomain domain = new {{projectPrefix}}_OrderDomain();
domain.validateOrderData(records);
}
private static void onBeforeUpdate(List<Order> newRecords, Map<Id, Order> oldMap) {
// Detect changed status fields
List<Order> statusChanges = new List<Order>();
for (Order order : newRecords) {
if (order.Status != oldMap.get(order.Id).Status) {
statusChanges.add(order);
}
}
if (!statusChanges.isEmpty()) {
publishOrderStatusChangeEvent(statusChanges);
}
}
private static void publishOrderStatusChangeEvent(List<Order> orders) {
// Publish Platform Event for async processing
}
}
Test Class Template with Factory Pattern
@IsTest
public class {{projectPrefix}}_CartServiceTest {
@TestSetup
static void setup() {
{{projectPrefix}}_TestDataFactory.createAccounts(10);
{{projectPrefix}}_TestDataFactory.createProducts(50);
}
@IsTest
static void testGetCart_Success() {
// Arrange
Account acc = {{projectPrefix}}_TestDataFactory.createAccount('Test Account');
Order cart = {{projectPrefix}}_TestDataFactory.createOrder(acc.Id);
// Act
{{projectPrefix}}_CartService service = new {{projectPrefix}}_CartService();
CartDTO result = service.getCart(cart.Id);
// Assert
Assert.isNotNull(result);
Assert.areEqual(cart.Id, result.id);
}
@IsTest
static void testAddItem_InvalidQuantity() {
// Arrange
Account acc = {{projectPrefix}}_TestDataFactory.createAccount('Test');
Order cart = {{projectPrefix}}_TestDataFactory.createOrder(acc.Id);
{{projectPrefix}}_CartService service = new {{projectPrefix}}_CartService();
// Act & Assert
try {
service.addItem(cart.Id, 'invalid', -1);
Assert.fail('Expected exception');
} catch ({{projectPrefix}}_ValidationException e) {
Assert.isTrue(e.getMessage().contains('Quantity'));
}
}
@IsTest
static void testBulkOperations() {
// Test with 200 items to verify bulkification
List<Order> carts = {{projectPrefix}}_TestDataFactory.createOrders(10);
{{projectPrefix}}_CartService service = new {{projectPrefix}}_CartService();
// No governor limit violations
Integer count = carts.size();
Assert.areEqual(10, count);
}
}
Integration Callout Class
public class {{projectPrefix}}_ERPIntegration extends HttpCalloutService {
private String namedCredential = 'callout:ERP_API';
public ERPOrderResponse createSalesOrder(String payload) {
try {
HttpResponse response = execute(payload);
if (response.getStatusCode() == 200) {
return parseResponse(response.getBody());
} else {
throw new {{projectPrefix}}_IntegrationException('ERP error: ' + response.getStatus());
}
} catch (HttpCalloutException e) {
publishErrorEvent('ERPIntegration', e.getMessage());
throw new {{projectPrefix}}_IntegrationException('HTTP callout failed: ' + e.getMessage());
}
}
protected override HttpResponse executeWithRetry(String body, Integer attempt) {
if (attempt > maxRetries) {
throw new {{projectPrefix}}_IntegrationException('Max retries exceeded');
}
HttpRequest req = new HttpRequest();
req.setEndpoint(namedCredential + '/api/salesorders');
req.setMethod(method);
req.setHeader('Content-Type', 'application/json');
req.setBody(body);
try {
Http http = new Http();
HttpResponse response = http.send(req);
return response;
} catch (System.CalloutException e) {
if (attempt < maxRetries) {
Integer delay = (Integer)Math.pow(2, attempt) * retryDelayMs;
System.debug('Retrying in ' + delay + 'ms');
return executeWithRetry(body, attempt + 1);
}
throw e;
}
}
}
Testing Standards
Coverage Requirements
- Minimum 85% coverage across all Apex classes
- 100% coverage for Services and critical Selectors
- 80% coverage for Controllers (AuraEnabled methods)
- 90% coverage for Triggers and Domains
Test Data Factory Pattern
public class {{projectPrefix}}_TestDataFactory {
public static Account createAccount(String name) {
Account acc = new Account(Name = name);
insert acc;
return acc;
}
public static List<Account> createAccounts(Integer count) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(new Account(Name = 'Account ' + i));
}
insert accounts;
return accounts;
}
public static Order createOrder(Id accountId) {
Order order = new Order(
AccountId = accountId,
EffectiveDate = Date.today(),
Status = 'Draft'
);
insert order;
return order;
}
}
Mock Callout Classes
@IsTest
global class MockERPCallout implements HttpCalloutMock {
global HttpResponse respond(HttpRequest request) {
HttpResponse response = new HttpResponse();
response.setHeader('Content-Type', 'application/json');
response.setBody('{"orderId":"ERP-12345","status":"created"}');
response.setStatusCode(200);
return response;
}
}
Assert Patterns
// Correct
Assert.areEqual(expectedValue, actualValue, 'Message on failure');
Assert.isNotNull(actual, 'Object should not be null');
Assert.isTrue(condition, 'Condition should be true');
// Never use (deprecated)
System.assertEquals(expected, actual);
System.assert(condition);
Bulk Test Scenarios
@IsTest
static void testBulkInsert200Items() {
List<Order_Item__c> items = new List<Order_Item__c>();
for (Integer i = 0; i < 200; i++) {
items.add(new Order_Item__c(
Quantity__c = 1,
Product__c = productId
));
}
insert items;
// Verify no SOQL in loops, efficient DML
}
Error Handling
Custom Exception Hierarchy
public class {{projectPrefix}}_BaseException extends Exception {}
public class {{projectPrefix}}_IntegrationException extends {{projectPrefix}}_BaseException {}
public class {{projectPrefix}}_ValidationException extends {{projectPrefix}}_BaseException {}
public class {{projectPrefix}}_CartException extends {{projectPrefix}}_BaseException {}
public class {{projectPrefix}}_InventoryException extends {{projectPrefix}}_BaseException {}
Structured Logging
public class {{projectPrefix}}_Logger {
public static void error(String component, String message, Exception e) {
Log__c log = new Log__c(
Component__c = component,
Message__c = message,
Stack_Trace__c = e.getStackTraceString(),
Severity__c = 'ERROR'
);
insert log;
}
}
Error Event Publishing
public void publishErrorEvent(String component, String message) {
{{projectPrefix}}_ErrorEvent__e event = new {{projectPrefix}}_ErrorEvent__e(
Component__c = component,
Message__c = message,
Timestamp__c = DateTime.now()
);
EventBus.publish(event);
}
Governor Limits
SOQL in Loops Prevention
// Bad: SOQL in loop (1000 queries possible)
for (Order order : orders) {
List<OrderItem> items = [SELECT Id FROM OrderItem WHERE OrderId = :order.Id];
}
// Good: Single SOQL outside loop
Map<Id, List<OrderItem>> itemsByOrderId = new Map<Id, List<OrderItem>>();
for (OrderItem item : [SELECT Id, OrderId FROM OrderItem WHERE OrderId IN :orderIds]) {
if (!itemsByOrderId.containsKey(item.OrderId)) {
itemsByOrderId.put(item.OrderId, new List<OrderItem>());
}
itemsByOrderId.get(item.OrderId).add(item);
}
Bulkification Patterns
// Process all records in one operation
public static void processOrders(List<Order> orders) {
// One query for all orders' items
Map<Id, List<OrderItem>> itemsByOrder = selectOrderItems(orders);
// One DML operation
List<Order> updatedOrders = new List<Order>();
for (Order order : orders) {
order.Item_Count__c = itemsByOrder.get(order.Id).size();
updatedOrders.add(order);
}
update updatedOrders;
}
Async Processing for Large Operations
// Batch for processing large pricebook entries
public class {{projectPrefix}}_PricebookSyncBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM PricebookEntry LIMIT 50000');
}
public void execute(Database.BatchableContext bc, List<PricebookEntry> scope) {
// Process in chunks of 200
}
public void finish(Database.BatchableContext bc) {}
}
// Scheduled for nightly execution
public class {{projectPrefix}}_PricebookSyncSchedulable implements Schedulable {
public void execute(SchedulableContext sc) {
Database.executeBatch(new {{projectPrefix}}_PricebookSyncBatch(), 200);
}
}
Heap Size Management for Pricebook Operations
// Avoid loading full pricebook into memory at once
// Use Batch Apex (6MB heap per batch)
// Stream processing with Database.getQueryLocator
// Chunk size 200 for optimal memory/throughput
public class {{projectPrefix}}_PricebookSyncBatch implements Database.Batchable<SObject>, Database.Stateful {
public Integer totalProcessed = 0;
public void execute(Database.BatchableContext bc, List<PricebookEntry> scope) {
// Process chunk of 200 entries
List<PricebookEntry> processed = new List<PricebookEntry>();
for (PricebookEntry entry : scope) {
// Transform and update
processed.add(entry);
}
update processed;
totalProcessed += scope.size();
}
}
Key Principles
- Never violate SSD pattern - Every class has one responsibility
- Test first - Write tests before code, aim for 85%+ coverage
- Bulkify everything - Handle 200+ records without governor violations
- Async by default - Use Batch/Queueable for large operations
- Log and monitor - Every integration failure must log to error tracking
- Security first - Always use
with sharing, validate input, escape output - Governor limits - Know your 5 SOQL, 150 DML, 6MB heap, 120s timeouts