Skip to main content

Apply Service Layer Principles in Apex

Learning Objectives

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

  • Create a Service Apex class and make effective use of it in your application.
  • Expose a Service Apex class as an API.

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 38:48 minute mark, in case you want to rewind and watch the beginning of the step again.)

Creating Services

Let's jump into some code! If you’re using a full encapsulation of the database and state management, one implementation approach is to use an appropriately named class with static methods representing the operations of the service.

The methods in your class represent the service operations, which access the information they need through the environment and parameters passed. The logic in the method updates the database or returns information in the method’s return type using custom Apex exceptions to indicate failure. The following example shows a service to apply a given discount to a set of Opportunities (and lines items, if present).

public with sharing class OpportunitiesService {
   public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) {
        // Validate parameters
        if(opportunityIds==null || opportunityIds.size()==0)
            throw new OpportunityServiceException('Opportunities not specified.');
        if(discountPercentage<0 || discountPercentage>100)
            throw new OpportunityServiceException('Invalid discount to apply.');
        // Query Opportunities and Lines (SOQL inlined for this example, see Selector pattern in later module)
        List<Opportunity> opportunities =
            [SELECT Amount, (SELECT UnitPrice FROM OpportunityLineItems)
             FROM Opportunity WHERE Id IN :opportunityIds];
        // Update Opportunities and Lines (if present)
        List<Opportunity> oppsToUpdate = new List<Opportunity>();
        List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>();
        Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100);
        for(Opportunity opportunity : opportunities) {
            // Apply to Opportunity Amount
            if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) {
                for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) {
                    oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor;
                    oppLinesToUpdate.add(oppLineItem);
                }
            } else {
                opportunity.Amount = opportunity.Amount * factor;
                oppsToUpdate.add(opportunity);
            }
        }
        // Update the database
        SavePoint sp = Database.setSavePoint();
        try {
            update oppLinesToUpdate;
            update oppsToUpdate;
        } catch (Exception e) {
            // Rollback
            Database.rollback(sp);
            // Throw exception on to caller
            throw e;
        }
    }
    public class OpportunityServiceException extends Exception {}
}

If we apply the configuration design consideration described earlier, we can add an overloaded version of the above service with an Options parameter, which allows the caller to instruct the service to skip committing the work. Returning the discounted values allows the client to implement a preview of the discounts that would be applied.

public static List<Decimal> applyDiscounts(
     List<Id> opportunityIds, Decimal discountPercentage, Options config)

The method signature in the full example above takes a list of IDs, which is also per the design considerations. However, only a single parameter for the discount was used. The assumption is that the same discount is applied to all opportunities. However, if allowing different discounts per opportunity is required, you can use a parameter class, as shown here.

public class OpportunityService {
    public class ApplyDiscountInfo {
        public Id OpportunityId;
        public Decimal DiscountPercentage;
    }
    public static void applyDiscounts(List<ApplyDiscountInfo> discInfos) {
      // Implementation...
    }
}

Exposing Services as APIs

Everyone loves APIs! Exposing your service logic to external parties through an API into your application is a must to develop a strong ecosystem of innovation and partner integrations around your products.

If you consider your service layer as fully tested, robust, and open to any client—and why wouldn’t it be because you’re using it as well, right?—then the simplest way to expose it to Apex developers is to modify the class and method modifiers from public to global. Boom!

global class OpportunityService {
    global class ApplyDiscountInfo {
        global Id OpportunityId;
        global Decimal DiscountPercentage;
    }
    global static void applyDiscounts(List<ApplyDiscountInfo> discInfos) {
       // Implementation...
    }
}

However, nothing in life is ever as easy as it sounds. If you’re creating an AppExchange package, using global has implications when changing method signatures between releases. Make sure you understand these implications.

It’s also worth considering exposing your API for off-platform callers, such as mobile or IoT. One way to do this is via the REST protocol. Here is a custom Apex REST API implementation.

@RestResource(urlMapping='/opportunity/*/applydiscount')
global with sharing class OpportunityApplyDiscountResource {
    @HttpPost
    global static void applyDiscount(Decimal discountPercentage) {
        // Parse context
        RestRequest req = RestContext.request;
        String[] uriParts = req.requestURI.split('/');
        Id opportunityId = uriParts[2];
        // Call the service
        OpportunitiesService.applyDiscounts(
            new Set<Id> { opportunityId }, discountPercentage);
    }
}

The ID of the opportunity is taken from the URI, and the discount percentage is taken from the posted information. As with the JavaScript Remoting example in the prior unit, exception handling is left to the caller. The platform marshalls exceptions into the appropriate JSON or XML response.

Pro Tip: Consider exposing some invocable methods so that users of the Flow Builder can access your Service layer functionality without writing code. Aren't platforms awesome!

Resources

Salesforce 도움말에서 Trailhead 피드백을 공유하세요.

Trailhead에 관한 여러분의 의견에 귀 기울이겠습니다. 이제 Salesforce 도움말 사이트에서 언제든지 새로운 피드백 양식을 작성할 수 있습니다.

자세히 알아보기 의견 공유하기