What is the Domain Layer?
The Domain Layer is "an object model of the domain that incorporates both behavior and data". - Martin Fowler
In most coding languages you need to connect to the database, query for the data and then you create wrapper classes to represent each underlying table in your database(s) to allow you to define how that particular table (object) should behave. Salesforce, however, already does a lot of this for you. For instance, there is no need to connect to a database; declarative behavior for your tables (objects) is already represented and your tables (objects) already have wrapper classes pre-defined for them (Ex: Contact cont = new Contact()).
However, the logic represented in a trigger is an exception to this rule. Apex triggers represent a unique scenario on the Salesforce platform: they are necessary for complex logic, but inherently they do not abide by any object-oriented principles. You can't create public methods in them, you can't unit test them, you can't re-use logic placed directly in a trigger anywhere else in your system, and so on. All of which is a massive detriment we need to overcome. That's where the domain layer comes into play.
The Domain Layer will allow you, on an object-by-object basis, to have an object-oriented approach to centralizing your logic. Basically, logic specific to a single object will be located in one place and only one place by using the domain layer. This ensures your logic specific to a single object isn't split into a ton of different places across your org.
The one comparison that justifies the library
Below is a real trigger of the kind that shows up in almost every org. It works. Somehow, it passed a code review. It also queries inside a loop, mixes validation with side effects, and cannot be unit tested without inserting records.
trigger AccountTrigger on Account (before insert, after update) {
if (Trigger.isBefore && Trigger.isInsert) {
for (Account a : Trigger.new) {
// validation, buried in a loop
if (a.AnnualRevenue == null) {
a.addError('Revenue is required');
}
// SOQL inside a loop - governor limits are one release away
List<Contact> cons = [
SELECT Id, Email FROM Contact WHERE AccountId = :a.Id
];
if (!cons.isEmpty()) {
a.Description = 'Has ' + cons.size() + ' contacts';
}
}
}
if (Trigger.isAfter && Trigger.isUpdate) {
List<Task> tasks = new List<Task>();
for (Account a : Trigger.new) {
if (a.Rating != Trigger.oldMap.get(a.Id).Rating) {
tasks.add(new Task(WhatId = a.Id, Subject = 'Rating changed'));
}
}
insert tasks; // DML with no transaction owner
// ... 340 more lines of the same, five developers deep
}
}
The right-hand version is four files instead of one, which feels like more work initially until you have to start updating your trigger and merge functionality from multiple apps within it. At that point things start to become difficult to manage, testing becomes challenging, and the need to structure in an easier to upkeep manner becomes more apparent. That's where the Apex Common Library comes in handy. Implement your domain class, never update the trigger itself again, and gain the flexibility of creating distinct methods that you can easily test and call from wherever you need them.
When to make a new Domain Layer Class
Basically, at the very least, anytime you need to make a trigger on an object you should implement a Domain Class. However, this is a bit generalized. Sometimes you don't actually need a trigger on an object, but you have object-specific behavior that should be implemented in a Domain class. For instance, if you have an object that doesn't need a trigger, but it has a very specific way it should have its tasks created, you should probably create a Domain Layer class for that object and put that task creation behavior there.
A domain layer class is essentially a mixture of a trigger handler class and a class that represents object-specific behaviors.
Where should you leverage the domain layer in your code?
You should only ever call to the domain layer code from service class methods or from other domain class methods. Controllers, batch classes and so on should never call out to the domain directly.
Domain Class Naming Conventions
Class Names - Domain classes should be named as the plural of whatever object you are creating a domain layer for. For instance, if you were creating a domain layer class for the Case object, the class would be declared as follows: public inherited sharing class Cases. This indicates that the class should be bulkified and handles multiple records, not a single object record.
Class Constructor - The constructor of these classes should always accept a list of records. This list of records will be leveraged by all of the methods within the domain class. This will be further explained below.
Method Names - Method names for database transactions should use the onTransactionName naming convention (Example: onAfterInsert). If the method is not related to a database transaction it should be descriptive to indicate what domain logic is being executed within it (Example: determineCaseStatus).
Parameter Names and Types - You do not typically need to pass anything into your domain layer methods. They should primarily operate on the list of records passed in the constructor in the majority of situations. However, some behavior-based (non-trigger-invoked) methods may need other domain objects and/or units of work passed to them. This will be further explained in the sections below.
Domain Layer Best Practices
Transaction Management
In the event you are actually performing DML operations in your Domain class, you should either create a Unit of Work or have one passed into the method doing the DML to appropriately manage your transaction. In the event you are not wanting to leverage the unit of work pattern you should make sure to at the very least set your System.Savepoint savePoint = Database.setSavePoint(); prior to doing your DML statement and use a try catch block to rollback if the DML fails.
Implementing the Domain Layer
To find out how to implement the Domain Layer using Apex Common, continue reading here: Implementing the Domain Layer with the Apex Common Library. If you're not interested in utilizing the Apex Common Library for this layer you can implement really any trigger framework and the core of the domain layer will be covered.
Libraries That Could Be Used for the Domain Layer
Apex Common (Contains a framework for all layers)
Apex Trigger Actions Framework
Domain Layer Examples
Apex Common Examples (Suggested)
Case Object Domain Layer Example
Contact Object Domain Layer Example
SFDC Trigger Framework Example