Conversation
CI deploy fails when __tests__ bundles reference @lwc/engine-dom, which is not valid org metadata.
daveespo
left a comment
There was a problem hiding this comment.
@daveespo reviewed 2 files and all commit messages, and made 5 comments.
Reviewable status: 2 of 186 files reviewed, 5 unresolved discussions (waiting on afawcett, ImJohnMDaniel, and john-storey-devops).
README.md line 33 at r1 (raw file):
------------------ This sample uses **concrete** Domain, Selector, and Service classes with static `newInstance()` factories and `@TestVisible` mock hooks for unit tests. There is no `Application.cls` dependency-injection factory — see [Apex Enterprise Patterns: Recent Updates and Thoughts on the Application Class](https://andyinthecloud.com/2026/04/13/apex-enterprise-patterns-recent-updates-and-thoughts-on-the-application-class/) for background on this approach.
Why keep the Interfaces for service/domain/selector in the project if you're taking the concrete implementation approach?
sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls line 39 at r1 (raw file):
* @return Unit of Work instance. */ public static fflib_ISObjectUnitOfWork newInstance() {
Hmm. Seeing this in real life doesn't look like what I expected ... notably, you are hard-coding the list of SObjects that are part of UOW which doesn't lend itself well to being able to reuse this UOW across functional domains. Selectors and Domains are tied to their underlying SObjects but the UOW is used across the Services and by hardcoding this list here, it seems like we fell back toward the 'Application is the God class' problem .. we just moved it into UOW
I guess I wonder why we don't take the list of SObjects as the argument to newInstance()
sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls line 45 at r1 (raw file):
List<Result> results = new List<Result>(); for (Request request : requests) { OpportunitiesService.newInstance().applyDiscounts(
Is this DML in a loop?
sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls line 36 at r1 (raw file):
* (for example 10 for 10%). */ public Decimal DiscountPercentage { get; set; }
lower case D
sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls line 35 at r1 (raw file):
* Discount percent entered on the list page (for example 10 for 10%). */ public Decimal DiscountPercentage { get; set; }
lowercase D
Aligns the sample factory with the concrete-class approach discussed in PR review.
|
@daveespo Good catch — I meant the sample app’s own domain/selector/service interfaces, which this PR removes. The remaining interfaces are from fflib-apex-common ( We can’t remove those without breaking the library contract, and they’re still useful where you want to mock at the framework boundary. The sample’s shift is to drop the extra sample-layer interfaces and On |
|
@daveespo I see the concern about the hardcoded SObject list feeling like a smaller The central registry does mean a missing type surfaces as a compile-time dependency that ripples through the codebase. I think that’s less common as the app evolves than the old So the trade-off here is one app-level registry (now in |
|
@daveespo Good catch — the earlier version did call the service inside the loop. It now builds a |
|
@daveespo Addressed — renamed |
…flib_SObjectUnitOfWork consistently.
|
While considering current SOLID practices regarding this PR, two points have come to light: interfaces belong where variation genuinely exists, and constructor injection is the mechanism that actually satisfies dependency inversion. My remaining reservation is how dependencies get substituted in tests. This also bears on the UnitOfWork.cls discussion above. Each service currently calls UnitOfWork.newInstance() internally and commits its own, so the shared-transaction case the class doc describes is met by combining operations in one method rather than by passing an instance. Constructor injection would let the entry point build one Unit of Work and hand it to both services — which is the capability the central registry is there to support. Consider the following as an example of Apex following modern SOLID patterns: // Supporting multiple processors
// like Visa, American Express and Discover
// just as examples.
public interface ICardProcessor
{
ProcessorResult charge(Payment__c payment);
}
public inherited sharing class PaymentService
{
private final ICardProcessor processor;
private final PaymentsSelector selector;
private final fflib_ISObjectUnitOfWork uow;
// Entry point for unit tests
public PaymentService(ICardProcessor processor,
PaymentsSelector selector,
fflib_ISObjectUnitOfWork uow)
{
this.processor = processor;
this.selector = selector;
this.uow = uow;
}
// Entry point for business logic.
public static PaymentService newInstance(ICardProcessor processor)
{
return new PaymentService(processor,
PaymentsSelector.newInstance(),
UnitOfWork.newInstance());
}
public void capture(Set<Id> paymentIds)
{
List<Payment__c> payments = selector.selectById(paymentIds);
for (Payment__c payment : payments)
{
ProcessorResult result = processor.charge(payment);
payment.Status__c = result.isApproved ? 'Captured' : 'Declined';
uow.registerDirty(payment);
}
uow.commitWork();
}
}
@IsTest
private class PaymentServiceTest
{
@IsTest
private static void captureMarksApprovedPaymentAsCaptured()
{
// Given
...
// When
new PaymentService(processorMock, selectorMock, uowMock).capture(paymentIds);
// Then
...
}
} |
ImJohnMDaniel
left a comment
There was a problem hiding this comment.
I am assuming that if you wanted developers to use newInstance() methods in selectors and domains, then you would want the constructors to be private instead of public. Is that what you were thinking?
See README.md Architecture Notes
Make it clearer the connection between the two
'service' to 'opportunitiesSvc' improves readability of the code, e.g. opportunitiesSvc.applyDiscountsAndCreateInvoices
|
@john-storey-devops thanks I made most of your changes here. Services and Controllers now use constructor injection. Domains and UnitOfWOrk still use property injection - as they are stateful (domain within a method, uow's within a service method) and we have no factories for them in this basic sample representation. |
|
Previously, daveespo (David Esposito) wrote…
Done. |
|
Previously, daveespo (David Esposito) wrote…
Done. |
|
Previously, daveespo (David Esposito) wrote…
Done |
|
Previously, daveespo (David Esposito) wrote…
Done The sample-layer |
|
Previously, daveespo (David Esposito) wrote…
The hardcoded list is not a smaller |
|
@ImJohnMDaniel not what we were thinking — we want |
Summary
Updates the sample to reflect current recommendations—making the Application class and interfaces optional unless using DI—along with modernized UI (VF→LWC) and Agentforce actions.
Application.clsand service interfaces in favour of concrete services withnewInstance()factories and test mocks — see Apex Enterprise Patterns: Recent Updates and Thoughts on the Application Class for background on this changeApplication.clstoUnitOfWork.cls(user mode enforcement unchanged frommaster)InvoiceTargets__mdtThis change is