Package Boundary Patterns
Introduction
Defining clear package boundaries is crucial for maintainable, scalable Salesforce solutions. This document presents patterns for establishing and maintaining package boundaries based on production implementations managing 40+ packages.
Boundary Definition Strategies
1. Domain-Driven Boundaries
Packages align with business domains, containing all layers for that domain.
package-structure/
├── customer-management/
│ ├── domain/ # Customer, Contact domain logic
│ ├── services/ # Customer-specific services
│ ├── selectors/ # Customer data access
│ └── ui/ # Customer-specific UI
├── order-management/
│ ├── domain/ # Order, OrderItem logic
│ ├── services/ # Order processing
│ ├── selectors/ # Order data access
│ └── ui/ # Order UI components
└── inventory/
├── domain/ # Product, Stock logic
├── services/ # Inventory services
├── selectors/ # Inventory queries
└── ui/ # Inventory UIWhen to Use:
Clear business domain separation
Different teams own different domains
Domains have different change frequencies
2. Layer-Based Boundaries
Packages organized by architectural layers, cutting across domains.
package-structure/
├── common-ui/ # Shared UI components
├── common-services/ # Shared services
├── common-domain/ # Shared domain logic
├── common-data/ # Shared data access
└── common-integration/ # Shared integrationsWhen to Use:
High reuse across domains
Consistent technical patterns
Small team maintaining all domains
3. Capability-Based Boundaries
Packages provide specific capabilities independent of domain.
package-structure/
├── authentication/ # Auth capability
├── document-generation/ # Document capability
├── notification/ # Notification capability
├── analytics/ # Analytics capability
└── workflow/ # Workflow capabilityWhen to Use:
Cross-cutting concerns
Reusable across multiple orgs
Third-party integrations
Boundary Enforcement Patterns
1. Interface Segregation Pattern
Each package exposes minimal, focused interfaces.
// ❌ BAD: Fat interface
public interface ICustomerService {
Customer create(CustomerRequest req);
void update(Customer c);
void delete(Id customerId);
List<Customer> search(String criteria);
void merge(Id primary, Id secondary);
void validateAddress(Address addr);
CreditScore checkCredit(Id customerId);
List<Order> getOrders(Id customerId);
// ... 20 more methods
}
// ✅ GOOD: Segregated interfaces
public interface ICustomerCommandService {
Customer create(CustomerRequest req);
void update(Customer c);
void delete(Id customerId);
}
public interface ICustomerQueryService {
Customer getById(Id customerId);
List<Customer> search(SearchCriteria criteria);
}
public interface ICustomerValidationService {
ValidationResult validateCustomer(Customer c);
AddressValidation validateAddress(Address addr);
}2. Dependency Injection Pattern
Packages declare dependencies explicitly through constructor injection or property injection.
// Package service with explicit dependencies
public class OrderService implements IOrderService {
private final ICustomerQueryService customerService;
private final IInventoryService inventoryService;
private final IPricingService pricingService;
// Constructor injection
public OrderService() {
this(
(ICustomerQueryService) Application.Service.newInstance(ICustomerQueryService.class),
(IInventoryService) Application.Service.newInstance(IInventoryService.class),
(IPricingService) Application.Service.newInstance(IPricingService.class)
);
}
@TestVisible
private OrderService(
ICustomerQueryService customerService,
IInventoryService inventoryService,
IPricingService pricingService
) {
this.customerService = customerService;
this.inventoryService = inventoryService;
this.pricingService = pricingService;
}
}3. Package Registry Pattern
Central registry for package capabilities and services.
// Central package registry
public class PackageRegistry {
private static Map<String, PackageInfo> packages = new Map<String, PackageInfo>();
public class PackageInfo {
public String name;
public String version;
public Map<Type, Type> services;
public Set<String> capabilities;
}
// Package self-registration
static {
registerPackage('customer-management', new PackageInfo()
.withService(ICustomerService.class, CustomerServiceImpl.class)
.withCapability('CUSTOMER_CRUD')
.withCapability('CUSTOMER_SEARCH')
);
}
public static Object getService(Type serviceInterface) {
for (PackageInfo pkg : packages.values()) {
if (pkg.services.containsKey(serviceInterface)) {
return pkg.services.get(serviceInterface).newInstance();
}
}
return null;
}
}Cross-Package Communication Patterns
1. Service Mesh Pattern
Packages communicate through a service mesh layer that handles routing, security, and monitoring.
// Service mesh router
public class ServiceMesh {
public static ServiceResponse invoke(ServiceRequest request) {
// Routing logic
String targetPackage = resolvePackage(request.service);
// Security check
validateAccess(request.caller, targetPackage);
// Monitoring
Long startTime = System.currentTimeMillis();
try {
// Invoke service
Object service = PackageRegistry.getService(request.service);
Object result = invokeMethod(service, request.method, request.params);
// Log metrics
logMetrics(request, System.currentTimeMillis() - startTime);
return new ServiceResponse(result);
} catch (Exception e) {
handleError(request, e);
throw e;
}
}
}2. Event-Driven Communication
Packages communicate through Platform Events for loose coupling.
// Publishing package
public class OrderService {
public void completeOrder(Id orderId) {
// Business logic
processOrder(orderId);
// Publish event for other packages
EventBus.publish(new OrderCompleted__e(
OrderId__c = orderId,
CompletedDate__c = DateTime.now(),
TotalAmount__c = calculateTotal(orderId)
));
}
}
// Subscribing package
public class FulfillmentService {
// Platform Event trigger
public static void handleOrderCompleted(List<OrderCompleted__e> events) {
for (OrderCompleted__e event : events) {
createFulfillment(event.OrderId__c);
}
}
}3. Configuration-Driven Routing
Use Custom Metadata to configure package interactions.
// Custom Metadata: PackageRoute__mdt
// ServiceInterface__c: 'ICustomerService'
// Implementation__c: 'CustomerServiceImpl'
// Package__c: 'customer-management'
// IsActive__c: true
public class DynamicServiceFactory {
public static Object createService(Type serviceInterface) {
PackageRoute__mdt route = [
SELECT Implementation__c, Package__c
FROM PackageRoute__mdt
WHERE ServiceInterface__c = :serviceInterface.getName()
AND IsActive__c = true
LIMIT 1
];
if (route != null) {
Type implType = Type.forName(route.Package__c, route.Implementation__c);
return implType.newInstance();
}
return null;
}
}Package Versioning and Compatibility
1. Semantic Versioning Pattern
{
"package": "customer-management",
"versionNumber": "2.1.3.NEXT",
"dependencies": [
{
"package": "common",
"versionNumber": "1.3.2.LATEST" // Minimum version
}
]
}2. Backward Compatibility Pattern
// Version 1.0 interface
public interface ICustomerService {
Customer getCustomer(Id customerId);
}
// Version 2.0 - Backward compatible
public interface ICustomerServiceV2 extends ICustomerService {
Customer getCustomer(Id customerId); // Original method
Customer getCustomerWithHistory(Id customerId); // New method
}
// Implementation supports both versions
public class CustomerServiceImpl implements ICustomerServiceV2 {
public Customer getCustomer(Id customerId) {
return getCustomerWithHistory(customerId).current;
}
public CustomerWithHistory getCustomerWithHistory(Id customerId) {
// New implementation
}
}Testing Package Boundaries
1. Contract Testing
@IsTest
public class CustomerServiceContractTest {
@IsTest
static void testServiceContract() {
// Test that service implements expected interface
ICustomerService service = (ICustomerService)
Application.Service.newInstance(ICustomerService.class);
System.assertNotEquals(null, service, 'Service must be available');
// Test contract methods exist and work
Customer testCustomer = TestDataFactory.createCustomer();
Customer result = service.getCustomer(testCustomer.Id);
System.assertNotEquals(null, result, 'Service must return customer');
}
}2. Boundary Testing
@IsTest
public class PackageBoundaryTest {
@IsTest
static void testNoDirectDatabaseAccess() {
// Verify package doesn't directly access other package's objects
String packageName = 'customer-management';
Set<String> allowedObjects = PackageRegistry.getAllowedObjects(packageName);
for (String query : getPackageQueries(packageName)) {
String objectName = extractObjectFromQuery(query);
System.assert(
allowedObjects.contains(objectName),
'Package should not query ' + objectName
);
}
}
}Package Boundary Checklist
Design Phase
Implementation Phase
Testing Phase
Deployment Phase
Common Boundary Violations
1. Database Coupling
// ❌ BAD: Direct query to another package's object
List<Order__c> orders = [SELECT Id FROM Order__c WHERE CustomerId__c = :custId];
// ✅ GOOD: Use service interface
List<Order> orders = orderService.getOrdersForCustomer(custId);2. Shared Global Variables
// ❌ BAD: Global static variable
public static Map<Id, Customer> customerCache = new Map<Id, Customer>();
// ✅ GOOD: Package-private cache with service access
private static Map<Id, Customer> cache = new Map<Id, Customer>();
public Customer getCachedCustomer(Id customerId) {
// Controlled access
}3. Trigger Dependencies
// ❌ BAD: Trigger calls another package directly
trigger OrderTrigger on Order__c (after insert) {
CustomerPackage.CustomerService.updateCustomerOrders(Trigger.new);
}
// ✅ GOOD: Trigger publishes event
trigger OrderTrigger on Order__c (after insert) {
EventBus.publish(OrderEvents.created(Trigger.new));
}Conclusion
Clear package boundaries are essential for:
Maintainability: Changes isolated to packages
Scalability: Add packages without affecting others
Testability: Test packages in isolation
Deployability: Deploy packages independently
Team Autonomy: Teams own clear boundaries
The key is finding the right balance between isolation and practicality for your specific context.
Last updated
Was this helpful?