Modular Package Architecture
Intent
Enable large-scale Salesforce development through independent, composable packages that can be developed, tested, and deployed in isolation while maintaining clear boundaries and contracts between components.
Core Principles
1. Package Layering Strategy
The architecture follows a strict layered approach where dependencies flow in one direction:
Environment-Specific → Access Management → Feature Packages → Domain Packages → Core → Common/Frameworks2. Package Types and Responsibilities
Foundation Layer
common: Shared frameworks (fflib, logger) with no business logic
core-unpackaged: Base metadata that cannot be packaged
Core Layer
core: Central business services, selectors, and domain classes
Provides interfaces that other packages implement or consume
Contains the main Application class for dependency injection
Domain Packages
Feature-complete vertical slices (e.g.,
revenue,lead-management)Self-contained with their own Application_XX class
Depend only on core and common
Infrastructure Packages
env-specific-alias-pre: Environment-specific configuration
access-management: Security and permissions
ui: User interface components
Implementation Pattern
Package Application Class
Each package defines its own Application class following this pattern:
// Application_XX.cls where XX is package abbreviation
public class Application_RE {
// Package-specific Unit of Work
public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
new fflib_Application.UnitOfWorkFactory(
new List<SObjectType>{
// Package-specific objects
});
// Service implementations mapping
private static final Map<Type, Type> serviceImplementationTypeMapping =
new Map<Type, Type>{
IContractLineItemsService_RE.class => ContractLineItemsServiceImpl_RE.class
};
// Service factory for this package
public static final fflib_Application.ServiceFactory Service =
new fflib_Application.ServiceFactory(serviceImplementationTypeMapping);
// Selector factory for this package
public static final fflib_Application.SelectorFactory Selector =
new fflib_Application.SelectorFactory(
new Map<SObjectType, Type>{
ContractLineItem__c.SObjectType => ContractLineItemsSelector_RE.class
});
// Domain factory for this package
public static final fflib_Application.DomainFactory Domain =
new fflib_Application.DomainFactory(
Application_RE.Selector,
new Map<SObjectType, Type>{
ContractLineItem__c.SObjectType => ContractLineItems_RE.Constructor.class
});
}Service Interface Pattern
Services expose functionality through interfaces:
// In core package
public interface ICasesService {
void setEndTimeOnOpenStatusChanges(ICases cases);
void insertNewStatusChange(ICases cases);
}
// Implementation in core package
public class CasesServiceImpl implements ICasesService {
// Implementation details
}
// Consuming from another package
ICasesService casesService = (ICasesService) Application.Service.newInstance(ICasesService.class);Cross-Package Communication
Packages communicate through:
Service Interfaces - Synchronous calls through defined contracts
Platform Events - Asynchronous, loosely coupled communication
Custom Metadata - Configuration-driven behavior
Package Configuration
sfdx-project.json Structure
{
"packageDirectories": [
{
"path": "src-env-specific-alias-pre",
"package": "env-specific-alias-pre",
"aliasfy": true, // Environment-specific aliasing
"versionNumber": "1.1.0.NEXT"
},
{
"path": "./src/core",
"package": "core",
"seedMetadata": {
"path": "./src/core-unpackaged" // Unpackaged dependencies
},
"enableFHT": true, // Field History Tracking
"dependencies": [
{
"package": "common",
"versionNumber": "1.3.2.LATEST"
}
]
},
{
"path": "./src/service",
"package": "service",
"dependencies": [
{
"package": "common",
"versionNumber": "1.3.2.LATEST"
},
{
"package": "core",
"versionNumber": "1.8.27.LATEST"
}
]
}
]
}Environment-Specific Configuration
src-env-specific-alias-pre/
├── default/ # Default configuration
├── dev/ # Development-specific
├── staging/ # Staging-specific
└── prod/ # Production-specific
├── namedCredentials/
├── customMetadata/
└── settings/Deployment Strategy
Release Configuration
releaseName: all-packages
includeOnlyArtifacts:
- env-specific-alias-pre # Environment config first
- common # Frameworks
- core # Core business logic
- service # Domain services
- ui # UI components
- access-management # Permissions last
releasedefinitionProperties:
promotePackagesBeforeDeploymentToOrg: prod
skipIfAlreadyInstalled: truePackage Deployment Order
Environment-specific configuration (
aliasfy: true)Foundation packages (common, frameworks)
Core business packages
Feature packages
UI packages (
alwaysDeploy: true)Access management packages
Benefits
Development Benefits
Parallel Development: Teams work on packages independently
Clear Ownership: Each package has defined boundaries
Faster CI/CD: Deploy only changed packages
Isolated Testing: Test packages in isolation with mocks
Architectural Benefits
Loose Coupling: Packages communicate through interfaces
High Cohesion: Related functionality grouped together
Reusability: Packages can be shared across orgs
Maintainability: Changes isolated to specific packages
Anti-Patterns to Avoid
1. Circular Dependencies
// ❌ BAD: Package A depends on B, B depends on A
// Package A
public class ServiceA {
ServiceB b = new ServiceB(); // Direct dependency
}
// Package B
public class ServiceB {
ServiceA a = new ServiceA(); // Circular!
}
// ✅ GOOD: Use interfaces and events
// Package A implements interface from Common
public class ServiceA implements IServiceA {
// Publish event instead of direct call
EventBus.publish(new ServiceAEvent__e());
}2. Package Sprawl
// ❌ BAD: Too many small packages
src/
├── validate-email-package/ # 2 classes
├── format-phone-package/ # 1 class
├── calculate-tax-package/ # 3 classes
// ✅ GOOD: Cohesive domain packages
src/
├── contact-management/ # All contact-related functionality
├── validation/
├── formatting/
└── services/3. Hidden Coupling
// ❌ BAD: Packages coupled through shared state
// Package A writes to Custom Setting
CustomSetting__c.getInstance().Value__c = 'data';
// Package B reads from same Custom Setting
String value = CustomSetting__c.getInstance().Value__c;
// ✅ GOOD: Explicit service contract
public interface IConfigurationService {
String getValue(String key);
void setValue(String key, String value);
}Package Sizing Guidelines
When to Create a New Package
Distinct business domain (10+ related objects)
Different deployment cadence
Separate team ownership
Reusable across multiple orgs
When to Keep in Existing Package
Tightly coupled functionality
Shared transaction boundaries
< 5 objects or 20 classes
Same deployment lifecycle
Testing Strategy
Unit Testing
@IsTest
private class ServiceTest {
@IsTest
static void testWithMockedDependency() {
// Mock external package dependency
fflib_ApexMocks mocks = new fflib_ApexMocks();
IExternalService mockService = mocks.mock(IExternalService.class);
// Inject mock
Application.Service.setMock(IExternalService.class, mockService);
// Test in isolation
// ...
}
}Integration Testing
Deploy packages to scratch org in correct order
Run integration test suite across packages
Validate cross-package platform events
Migration Path
From Monolithic to Modular
Identify Boundaries: Map existing code to domains
Extract Interfaces: Define service contracts
Create Package Structure: Set up package directories
Move Code Incrementally: One domain at a time
Update Dependencies: Adjust sfdx-project.json
Test Thoroughly: Ensure no regression
Production Implementation Patterns
Successful implementations of this architecture demonstrate:
Dozens of packages managed independently in production systems
Clear separation between foundation, core, domain, and UI layers
Environment-specific configuration packages using
aliasfy: truefor environment-based deploymentPackage-specific Application classes following the Application_XX pattern for each domain
Successful scaling to large development teams working in parallel without conflicts
Related Patterns
Last updated
Was this helpful?