All Skills

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

A
$npx skills add Architect-And-Bot/sf-b2b-commerce-template --skill sfb2b-apex

Salesforce 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

  1. Never violate SSD pattern - Every class has one responsibility
  2. Test first - Write tests before code, aim for 85%+ coverage
  3. Bulkify everything - Handle 200+ records without governor violations
  4. Async by default - Use Batch/Queueable for large operations
  5. Log and monitor - Every integration failure must log to error tracking
  6. Security first - Always use with sharing, validate input, escape output
  7. Governor limits - Know your 5 SOQL, 150 DML, 6MB heap, 120s timeouts