1

Considering a SoC architecture where the following classes exist:

  • InvoicesService - Service Layer-
  • Invoices -Domain Layer-

Let's say, in a new InvoicesService method I need to insert Contact records. What do you think is the right place for locating this operation?

  1. Instantiate and insert the contact directly in this service (as showed in the attached code)

or

  1. Locate this code in a new ContactService or inside the Contacts Domain, and call this service from the InvoicesService?

In case of the invoice record, I guess this service is the right place, because of it is related to its context, am I right?

public with sharing class InvoiceService { 

    public static createInvoice(invoiceDTO invoiceDTO){
            
        //Create Contact
        Contact c   = new Contact();
        c.LastName  = invoiceDTO.LastName;
        c.Email     = invoiceDTO.Email;
        insert c;


        //Create Invoice
        Invoice__c invoice = new Invoice();
        invoice.Amount__c = invoiceDTO.Amount;
        invoice.Contact__c = c.Id;
        insert invoice;
    }
}
2
  • 2
    Just for clarity, "SoC" here refers to Separation of Concerns
    – Derek F
    Jul 29 at 19:03
  • @DerekF Thanks for that. I was confused why they were talking about System on a Chip.
    – sfdcfox
    Jul 29 at 19:05
2

A useful way to approach this would be (here I'm using the fflib pattern)

class ContactsServiceImpl implements IContactsService {

  // example using fflib UnitOfWork
  public void create(Invoice[] invoices) {
     fflib_ISObjectUNitOfWork uow = Application.UnitOfWork.newInstance();
     for (Invoice invoice: invoices) {
          uow.registerNew(new Contact (
             LastName = invoice.LastName,
             ...));
     }
     uow.commitWork();

 }
 // example w/o UnitOfWork
 public void create(Contract[] contracts) {
     Contact[] contacts = new List<Contact>();
     for (Contract contract: contracts) {
          contacts.add(new Contact (
             LastName = contract.CounterPartyName,
             ...));
     }
     insert contacts;
 }

 // example w UnitOfWork and arbitrary wrapper object
 public void create(IContactBuildable contactBuildables) {
     IContactBuildable[] buildables = new List<IContactBuildable>();
     for (Contract contract: contracts) {
          uow.registerNew(contactBuildable.make()));
     }
     insert contacts;
 }
}

interface IContactBuildable {
  Contact make();
}

public class SomeWrapper implements IContactBuildable {
  public SomeWrapper(...someargs...) {..}
  public Contact make() {
    return new Contact(LastName = this.lastName, // some obj variable
                       Email = getEmail() // some obj meth
                       ...);
  }
}

ContactsService.create(new List<SomeWrapper> {  // invoke svc
      new SomeWrapper(..some args),
      new SomeWrapper(..some other args ..)});  

Of course, you could have a common method create(SObject[] sobjects) that used instanceOf to dispatch/delegate to different Contact builders

The Contacts Domain layer is not the place to put the logic as the domain is responsible for managing existing instances of Contact (as presented to it by triggers) or other services. Remember, the fflib constructor for a domain is a list of sobjects (that is, already instantiated)

Doing the logic in the Invoice Service is plausible, especially if there are many different flavors of invoice-to-contact mapping and more context than just the Invoice object itself is required to figure out how to construct a Contact.

4
  • I love this approach, my only concern is to have a lot of create methods for different use cases with different input parameters, sObjects, wrapped classes…. Jul 29 at 22:21
  • well, you could have create(IContactBuildable[] objs) and have the method invoke an IContactBuildable method make() on each obj that returns a Contact - that way all the detailed logic is in the object that represents the use case.
    – cropredy
    Jul 29 at 22:28
  • Could you elaborate a little more your comment to understand better your solution? and how this solution is aligned with the Unit Of Work pattern, the one provided by fflib. Thanks! Jul 30 at 0:08
  • @lopez.regalado.fj - I edited my answer
    – cropredy
    Jul 30 at 4:10
1

In an standard textbook implementation, your InvoiceService would call a ContactService to create a Contact record; this ensures that the only way to create a contact is from a single place with a single implementation. In practice, this may be overkill (e.g. you may end up having a function that's only called once), but only you can determine if that's the case.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.