Domain Layer Code Examples
Overview
The Domain Layer encapsulates object-specific business logic and validation. This layer is responsible for enforcing business rules, data validation, and object-specific operations.
Related Patterns:
- Apex Class Layering - Architecture patterns
- Service Layer Examples - Service layer patterns
- Selector Layer Examples - Selector layer patterns
Problem: You need to validate Contact records and apply business rules before DML operations. The domain layer encapsulates Contact-specific logic.
Solution:
/**
* Domain class for Contact object
* Encapsulates Contact-specific business logic and validation
*/
public with sharing class ContactDomain {
/**
* Validates and prepares Contact records for update
* @param contacts List of Contact records to validate
* @throws ContactValidationException if validation fails
*/
public static void validateAndPrepareForUpdate(List<Contact> contacts) {
if (contacts == null || contacts.isEmpty()) {
return;
}
for (Contact contact : contacts) {
// Validate required fields
if (String.isBlank(contact.LastName)) {
throw new ContactValidationException('Last Name is required');
}
// Validate email format
if (String.isNotBlank(contact.Email) && !isValidEmail(contact.Email)) {
throw new ContactValidationException('Invalid email format: ' + contact.Email);
}
// Apply business rules
applyBusinessRules(contact);
}
}
/**
* Validates Contact for insert
* @param contacts List of Contact records to validate
*/
public static void validateForInsert(List<Contact> contacts) {
validateAndPrepareForUpdate(contacts);
// Additional insert-specific validation
for (Contact contact : contacts) {
// Check for duplicates
if (hasDuplicateEmail(contact.Email)) {
throw new ContactValidationException('Contact with this email already exists');
}
}
}
/**
* Applies business rules to Contact
* @param contact Contact record to update
*/
private static void applyBusinessRules(Contact contact) {
// Business rule: Set default values
if (String.isBlank(contact.Phone)) {
contact.Phone = 'Not Provided';
}
// Business rule: Format name
if (String.isNotBlank(contact.FirstName) && String.isNotBlank(contact.LastName)) {
contact.FullName__c = contact.FirstName + ' ' + contact.LastName;
}
}
/**
* Validates email format
* @param email Email address to validate
* @return true if valid email format
*/
private static Boolean isValidEmail(String email) {
String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
Pattern emailPattern = Pattern.compile(emailRegex);
return emailPattern.matcher(email).matches();
}
/**
* Checks if email already exists (simplified - would use Selector in real implementation)
* @param email Email to check
* @return true if duplicate exists
*/
private static Boolean hasDuplicateEmail(String email) {
// Note: In real implementation, delegate to Selector layer
// This is simplified for example
return false;
}
/**
* Custom exception for Contact validation errors
*/
public class ContactValidationException extends Exception {}
}
Explanation:
- Encapsulation: All Contact-specific logic in one place
- Validation: Validates required fields and formats
- Business Rules: Applies business rules (default values, formatting)
- Reusability: Can be called from triggers or Service layer
- No SOQL: Delegates data access to Selector layer (shown in simplified form)
Usage:
// In a trigger
trigger ContactTrigger on Contact (before insert, before update) {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
ContactDomain.validateForInsert(Trigger.new);
} else if (Trigger.isUpdate) {
ContactDomain.validateAndPrepareForUpdate(Trigger.new);
}
}
}
// Or from Service layer
List<Contact> contacts = ContactSelector.selectByIds(contactIds);
ContactDomain.validateAndPrepareForUpdate(contacts);
update contacts;
Test Example:
@isTest
private class ContactDomainTest {
@isTest
static void testValidateAndPrepareForUpdate_Success() {
List<Contact> contacts = new List<Contact>{
new Contact(LastName = 'Test1', Email = 'test1@example.com'),
new Contact(LastName = 'Test2', Email = 'test2@example.com')
};
Test.startTest();
ContactDomain.validateAndPrepareForUpdate(contacts);
Test.stopTest();
// Verify business rules applied
System.assertEquals('Not Provided', contacts[0].Phone, 'Default phone should be set');
}
@isTest
static void testValidateAndPrepareForUpdate_MissingLastName() {
List<Contact> contacts = new List<Contact>{
new Contact(Email = 'test@example.com')
// Missing LastName
};
Test.startTest();
try {
ContactDomain.validateAndPrepareForUpdate(contacts);
System.assert(false, 'Should throw exception');
} catch (ContactDomain.ContactValidationException e) {
System.assert(e.getMessage().contains('Last Name is required'), 'Should throw validation error');
}
Test.stopTest();
}
@isTest
static void testValidateAndPrepareForUpdate_InvalidEmail() {
List<Contact> contacts = new List<Contact>{
new Contact(LastName = 'Test', Email = 'invalid-email')
};
Test.startTest();
try {
ContactDomain.validateAndPrepareForUpdate(contacts);
System.assert(false, 'Should throw exception');
} catch (ContactDomain.ContactValidationException e) {
System.assert(e.getMessage().contains('Invalid email format'), 'Should throw email validation error');
}
Test.stopTest();
}
}
Example 2: Domain with Service Layer Integration
Pattern: Domain Layer Called from Service Layer
Use Case: Complex workflows with domain validation
Complexity: Intermediate
Problem: Service layer needs to validate and prepare records using domain logic before processing.
Solution:
/**
* Service layer using Domain layer for validation
*/
public with sharing class ContactUpdateService {
public static List<Id> processContacts(Set<Id> contactIds) {
// 1. Query using Selector
List<Contact> contacts = ContactSelector.selectByIds(contactIds);
// 2. Validate using Domain layer
ContactDomain.validateAndPrepareForUpdate(contacts);
// 3. Perform DML
update contacts;
// 4. Return processed IDs
List<Id> processedIds = new List<Id>();
for (Contact c : contacts) {
processedIds.add(c.Id);
}
return processedIds;
}
}
Related Examples: Service Layer Examples
Example 3: Domain Used in Trigger
Pattern: Domain Layer Called Directly from Trigger
Use Case: Simple validation in triggers
Complexity: Basic
Problem: Trigger needs simple validation without complex orchestration.
Solution:
trigger ContactTrigger on Contact (before insert, before update) {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
ContactDomain.validateForInsert(Trigger.new);
} else if (Trigger.isUpdate) {
ContactDomain.validateAndPrepareForUpdate(Trigger.new);
}
}
}
Best Practices:
- Use Domain layer directly in triggers for simple validation
- Use Service layer for complex workflows
- Keep triggers thin (delegate to Domain or Service)
Common Patterns
Pattern 1: Validation Methods
public static void validateForInsert(List<Contact> contacts) {
// Insert-specific validation
}
public static void validateForUpdate(List<Contact> contacts) {
// Update-specific validation
}
public static void validateAndPrepareForUpdate(List<Contact> contacts) {
// Validation + business rule application
}
Pattern 2: Business Rule Application
private static void applyBusinessRules(Contact contact) {
// Set default values
// Calculate derived fields
// Apply formatting
// Enforce business constraints
}
Pattern 3: Helper Methods
private static Boolean isValidEmail(String email) {
// Validation logic
}
private static String formatPhone(String phone) {
// Formatting logic
}
Best Practices
- Encapsulate object-specific logic in Domain layer
- Validate before DML operations
- Apply business rules consistently
- Do NOT contain SOQL (delegate to Selector layer)
- Do NOT contain external callouts (delegate to Integration layer)
- Can be called from triggers OR Service layer
- Use custom exceptions for validation errors
- Keep methods focused on single responsibility
Related Patterns
- Apex Patterns - Complete Apex patterns