Skip to main content

Learn Service Layer Principles

Learning Objectives

After completing this unit, you’ll be able to:

  • Explain the origins of the Service pattern from Martin Fowler’s Enterprise Application Architecture patterns.
  • Determine which Apex code belongs in the Service layer.
  • Discuss how the Service layer fits within your application architecture and the platform.
  • Design a Service layer to work within the platform’s best practices.

Follow Along with Trail Together

Want to follow along with an expert as you work through this step? Take a look at this video, part of the Trail Together series.

(This clip starts at the 17:45 minute mark, in case you want to rewind and watch the beginning of the step again.)

Introduction

The previous unit introduced SOC as a means to focus software architects into thinking about layering application logic. This unit focuses on defining and utilizing the Service layer as a key entry point to other layers and consumers (such as an API) of your application.

Service layer, "Defines an application's boundary with a layer of services that establishes a set of available operations and coordinates the application's response in each operation." Martin Fowler / Randy Stafford, EAA Patterns

The Service layer helps you form a clear and strict encapsulation of code implementing business tasks, calculations and processes. It’s important to ensure that the Service layer is ready for use in different contexts, such as mobile applications, UI forms, rich web UIs, and numerous APIs. It must remain pure and abstract to endure the changing times and demands ahead of it. The following sections define the guidelines for creating a Service layer implementation in Apex while also keeping in mind Salesforce best practices and governor limits.

Who Uses the Service Layer?

You might be tempted to say "all the cool kids" use a service layer, but technically the consumer of a service layer is called the "client." A client invokes your service layer code. These are not human interactions with the Service layer, but other pieces of code that interact with the user or system, such as a UI controller or Batch Apex.

An example of a client is code written within a Visualforce controller or @AuraEnabled method. However, there are many other clients (consumers) of your service layer code to consider. To build a list of candidates, think about all the ways in which Apex logic on the Salesforce platform can be invoked.

Ways in which Apex logic on the Salesforce platform can be invoked: Apex UI Controllers, Apex Web Services, Apex REST Services, Invocable Methods, Inbound Email Handlers, Batch Apex, Scheduled Apex and Queueable.

Note

Note

Apex triggers are missing because the logic belongs to your application’s Domain layer, which is closely aligned with the objects and thus manipulation of the records in your application. Domain logic is called both directly and indirectly within the Service layer and, of course, via the platform UI and APIs.

As you can imagine, it’s all too easy to leak service layer logic into the Apex code created for other layers and purposes. Leaks erode the value of implementing a Service layer because they give rise to inconsistencies filtering up to your end-users experience. For example, as users interact with your application through a given feature exposed through several of the Salesforce technologies. Such as a certain calculation that is exposed via a Lightning Component you have built as well as through an Apex Rest Service. The behavior in both cases needs to be consistent. In the next section, we discuss the design and responsibilities of the service layer and the expectations that code consuming the service layer should have.

Platform Innovation and Adaptability

The technologies listed above have been gradually introduced as new features on the platform over years. Imagine if you had written code in such a way that ties it to a specific feature and you had to refactor each time. Now imagine how much easier it would be to adopt and adapt your application to these features and upcoming ones if you didn’t have to worry about refactoring your code from one of the previous areas first. Or worse, duplicating the code, because you fear refactoring will break existing functionality. Yuck.

Design Considerations

  • Naming conventions - The Service layer must be abstract enough to be meaningful to a number of clients. This aspect often falls to the verbs and nouns you use in class, methods, and parameter names. Ensure that they are expressed in general terms of the application or task rather than relating to a specific client caller. For example, this method name is based on business operation InvoiceService.calculateTax(...) while this method name is based on a specific client usage operation InvoiceService.handleTaxCodeForACME(...). The second method name should leave you feeling a little uneasy.
  • Platform / Caller sympathy - Design method signatures that support the platform’s best practices, especially bulkification. One of the main concerns of all code on Salesforce is bulkification. Consider services that can be called with lists versus single parameter sets. For example, this method's parameters allow for bulkification InvoiceService.calculateTax(List<TaxCalculation> taxCalculations) while this method forces callers to call the method repeatedly InvoiceService.calculateTax(Invoice invoice, TaxInfo taxCodeInfo). Again, the second one should make you feel a little uneasy.
  • SOC considerations - Service layer code encapsulates task or process logic typically utilizing multiple objects in your application. Think of this as an orchestrator. In contrast, code relating specifically to validation, field values or calculations, which occur during record inserts, updates, and deletes, is the concern of the related object. Such code is typically written in Apex triggers and can remain there. Don't worry, we’ll introduce the Domain pattern for this type of code shortly.
  • Security - Service layer code and the code it calls should by default run with user security applied. To ensure that this is the case, utilize the with sharing modifier on your Apex Service classes (especially important if you’re exposing such code via the global modifier). If Apex logic must access records outside of the user’s visibility, the code must explicitly elevate the execution context as briefly as possible. A good approach is to use a private Apex inner class with the without sharing modifier applied.
  • Marshalling - Avoid prescribing how aspects of interacting with the service layer are handled because certain aspects are better left to the callers of your service, for example, semantics like error handling and messaging. Callers often have their own means to interpret and handle these. For example, Visualforce uses <apex:pagemessages>, and Schedule jobs will likely use emails, Chatter posts, or logs to communicate errors. So in this case, it is typically best to leverage the default error-handling semantics of Apex by throwing exceptions. Alternatively, your service can provide partial database update feedback to the caller. In this case, devise an appropriate Apex class and return a list of that type. The system Database.insert method is a good example of this type of method signature.
  • Compound services - Although clients can execute multiple service calls one after another, doing so can be inefficient and cause database transactional issues. It’s better to create compound services that internally group multiple service calls together in one service call. It is also important to ensure that the service layer is as optimized as possible in respect to SOQL and DML usage. This does not mean that more granular services cannot be exposed; it just means that you should give the callers the option to use a more specific single service if needed.
  • Transaction management and statelessness - Clients of the service layer often have different requirements regarding the longevity of the process being undertaken and the information being managed. For example, a single request to the server and multiple requests split into separate scopes: the manage state (such as Batch Apex) or a complex UI that maintains its own page state across several requests. Given these variations on state management, it’s best to encapsulate database operations and service state within the method call to the service layer. In other words, make the service stateless to give calling contexts the flexibility to employ their own state management solutions. The scope of a transaction with the database should also be contained within each service method so that the caller does not have to consider this with its own SavePoints, for example.
  • Configuration - You might have common configuration or behavioral overrides in a service layer, such as providing control to allow the client to instruct the server layer not to commit changes or send emails. This scenario might be useful in cases where the client is implementing preview or what-if type functionality. Be sure to consider how you implement this consistently, perhaps as a method overload that takes a shared Options parameter, similar to the DML methods in Apex.
Note

Note

In Apex, database transactions are auto-committed if the request completes without error and rolled back in the event of an unhandled exception. However, allowing a request to complete with errors being thrown is not a desired user experience because the platform handling of these exceptions is often not all that accessible (Batch Apex jobs) or aesthetically pleasing (white page, black text) to end users. For this reason, developers often catch exceptions and route them accordingly. A potential side effect with this approach is that the platform sees this is a valid completion of the request and commits records that have been inserted or updated leading up to the error that occurred. By following the above service layer design principles regarding statelessness and transaction management, you can avoid this problem.

Using Services in Apex

Let’s see how you can use the OpportunitiesService.applyDiscounts method from a number of places. Lightning components and Batch Apex are all shown below.

The following example deals with a single Opportunity selected via a Lightning component. Imagine that you have a Lightning component that prompts the user for a discount percentage to apply to a selected Opportunity Amount. Notice that error handling is done at this stage, not in the service, because Lightning components have their own way of surfacing errors.

@AuraEnabled
public void applyDiscount(Id opportunityId, Decimal discountPercentage) {
    try {
        // Apply discount entered to the current Opportunity
        OpportunitiesService.applyDiscounts(
            new Set<ID> { opportunityId }, discountPercentage);
    } catch(Exception e) {
        throw new AuraHandledException('Something went wrong: ' + e.getMessage());
    }
}

The following example deals with processing chunks of records via the Batch Apex execute method. If you look closely you'll notice that the exception handling is different from the earlier Lightning component example.

public with sharing class OpportunityApplyDiscountJob implements Database.Batchable<SObject> {
    public Decimal DiscountPercentage {get;private set;}
    public OpportunityApplyDiscountJob(Decimal discountPercentage) {
        // Discount to apply in this job
        this.DiscountPercentage = discountPercentage;
    }
    public Database.QueryLocator start(Database.BatchableContext ctx) {
        // Opportunities to discount
        return Database.getQueryLocator(
            'SELECT Id FROM Opportunity WHERE StageName = \'Negotiation/Review\'');
    }
    public void execute(Database.BatchableContext BC, List<sObject> scope) {
        try {
          // Call the service
          OpportunitiesService.applyDiscounts(
            new Map<Id,SObject>(scope).keySet(),DiscountPercentage);
        } catch (Exception e) {
            // Email error, log error, chatter error etc..
        }
    }
    public void finish(Database.BatchableContext ctx) { }
}

In a later unit, we’ll look at exposing a Service method via a REST API.

Other Benefits and Considerations of an Apex Service Layer

Outside the scope of this particular unit is the topic of implementing services for mock testing and parallel development. Services can use the factory pattern along with Apex interfaces to dynamically resolve the implementation as opposed to coding it directly in the methods. This approach is useful for giving more flexibility in engineering the scope of tests around services. However, factories require some plumbing and framework to create interfaces, ways to register classes, and other fun stuff in your code. Be certain that their use adds value in terms of mocking and runtime flexibility based on configurations.

Also, defining the service layer design upfront also allows developers or teams of developers to work better together or in parallel. Those needing to call the services can use dummy implementations to return static data, while those implementing the services can work on the code without impacting their callers. This style of development is often referred to as Design by Contract (Dbc) and is a beautiful thing.

Summary

Investing in a service layer for your application offers the engineering benefits of greater reuse and adaptability, as well as provides a cleaner and more cost effective way of implementing an API for your application, a must in today’s cloud-integrated world. By closely observing the encapsulation and design considerations described above, you start to form a durable core for your application that will endure and remain a solid investment throughout the ever-changing and innovative times ahead!

Resources

在 Salesforce 帮助中分享 Trailhead 反馈

我们很想听听您使用 Trailhead 的经验——您现在可以随时从 Salesforce 帮助网站访问新的反馈表单。

了解更多 继续分享反馈