SAP Commerce Service Layer Deep Dive — Part 5
Understanding Controller, Facade, Service, and DAO is straightforward. The harder part is deciding where a piece of logic belongs when a real SAP Commerce requirement starts growing.
Before We Start: OOTB vs Custom Classes
This article targets SAP Commerce 2211-family APIs. I’ll distinguish platform APIs from the custom classes created for the reorder example.
OOTB/platform examples: CartService, CommerceCartService, CommerceCartParameter, CommerceCartModification, ModelService, FlexibleSearchService, FlexibleSearchQuery, SearchResult, BusinessProcessService, Populator, ValidateInterceptor, InterceptorContext, JobPerformable/AbstractJobPerformable, PerformResult, and platform Models such as OrderModel, CartModel, and ProductModel.
Custom examples: ReorderService, ReorderValidator, ReorderEligibilityStrategy, ReorderOrderDao, AvailabilityService, InventoryAdapter, ReorderStrategyResolver, and ReorderQuantityCalculator.
Developer Note
If you cannot find one of the custom names in SAP Help, that is expected. It represents a responsibility we may introduce in our own extension; it is not presented as an OOTB SAP Commerce API.
Introduction
If you’ve worked on an SAP Commerce project for a while, you’ve probably seen this happen.
A requirement starts small.
Maybe the business asks for a button that allows a customer to reorder a previous Order.
The first implementation looks simple enough: retrieve the Order, check whether the customer can use it, validate the entries, add them to the Cart, recalculate, and return the result.
A developer adds a Controller method, calls a few existing SAP Commerce services, adds some validation, and the feature works.
Then the requirement grows.
OCC needs the same functionality. A new business rule says only certain Orders can be reordered. Current availability must be checked from an external system. Some Products may no longer be orderable. A scheduled process later needs part of the same capability.
And suddenly a question appears during code review:
Where should all of this logic actually live?
Should it stay in the Controller? Should we move it into the Facade? Should everything go into a Service? Should the validation become an Interceptor? Should FlexibleSearch move into a DAO? Should the external call have its own Service?
These aren’t really SAP Commerce syntax questions. They’re architecture questions.
In this article, rather than memorizing another architecture diagram, we’ll work through one feature and let the design evolve with the requirement. We’ll intentionally make some decisions that look reasonable at first, then see where they start causing problems.
1. The Requirement: Reorder a Previous Order
Let’s use a B2B commerce scenario. A customer opens Order History, selects a previously completed Order, and clicks Reorder.
At first, the expected flow looks like this:
- Retrieve the original Order.
- Verify that the current customer can access it.
- Check whether the Order can be reordered.
- Validate the Products and quantities.
- Add eligible entries to the current Cart.
- Recalculate the Cart.
- Return the result.
A developer could reasonably begin with a Controller like this:
@PostMapping("/orders/{orderCode}/reorder")public String reorder(@PathVariable final String orderCode, final Model model){ final OrderModel order = findOrder(orderCode); if (!order.getUser().equals(userService.getCurrentUser())) { throw new IllegalArgumentException("Order does not belong to current user"); } if (!isReorderAllowed(order)) { throw new IllegalStateException("Order is not eligible for reorder"); } for (final AbstractOrderEntryModel entry : order.getEntries()) { validateProduct(entry.getProduct()); final CommerceCartParameter parameter = new CommerceCartParameter(); parameter.setCart(cartService.getSessionCart()); parameter.setProduct(entry.getProduct()); parameter.setQuantity(entry.getQuantity()); parameter.setEnableHooks(true); commerceCartService.addToCart(parameter); } model.addAttribute("cart", cartService.getSessionCart()); return "pages/cart/cartPage";}
For a first implementation, nothing about this is particularly surprising. The requirement is visible in one place, the code is easy to follow, and most importantly: it works.
So why change it?
2. The Problem Isn’t That the Controller Is Long
When reviewing code like this, it’s tempting to say, “The Controller has too much code. Move it into a Facade.”
But line count isn’t really the problem.
The Controller now knows how to retrieve an Order, determine ownership, evaluate reorder eligibility, validate Products, create Cart entries, and decide when the Cart should be recalculated.
In other words, the HTTP entry point has started to understand the business operation itself.
That becomes painful when a headless application needs the same reorder capability through OCC. The business behavior is the same; only the entry point changed.
We don’t want the OCC Controller calling the storefront Controller, and copying the logic gives us two implementations of the same business rule.
Design Insight
A useful test for business logic is to ask whether it should survive when the entry point changes. Storefront, OCC, Backoffice, and scheduled jobs are different ways of reaching a capability. They shouldn’t automatically become different implementations of that capability.
3. So We Move Everything Into the Facade?
The natural next step is to introduce reorderFacade.reorder(orderCode).
@PostMapping("/orders/{orderCode}/reorder")public String reorder(@PathVariable final String orderCode, final Model model){ final ReorderData result = reorderFacade.reorder(orderCode); model.addAttribute("reorderResult", result); return "pages/cart/cartPage";}
The Controller now deals primarily with request parameters, invoking the operation, and preparing the response. That’s better.
But if DefaultReorderFacade now retrieves the Order, validates the customer, decides eligibility, checks inventory, updates the Cart, recalculates, and converts the result, we’ve only moved the problem.
Common Mistake
Moving business logic from a Controller into a Facade doesn’t automatically create clean architecture. If the Facade becomes responsible for the entire business operation, we’ve simply replaced a fat Controller with a fat Facade.
4. What Should the Facade Actually Do?
In SAP Commerce, a Facade is useful because the consumer of a capability often shouldn’t need to understand the ServiceLayer model behind it.
The storefront may want ReorderData. OCC may eventually map Data objects into WsDTOs. The underlying operation may work with Models and domain-oriented results.
public ReorderData reorder(final String orderCode){ final ReorderResult result = reorderService.reorder(orderCode); return reorderResultConverter.convert(result);}
Now the Facade exposes an operation in a form useful to its consumer without deciding every rule involved in reorder.
SAP Commerce already gives us familiar examples of this separation. A storefront can work with CartData and CartModificationData, while lower layers work with CartModel, ProductModel, CommerceCartParameter, and CommerceCartModification.
Design Insight
A Facade should make a business capability easier for another application layer to consume. It shouldn’t become the business capability itself.
5. The Service Should Represent the Business Capability
The usual advice is that business logic belongs in the Service layer. That’s useful, but incomplete.
If we interpret it as “everything that doesn’t fit elsewhere goes into DefaultSomethingService,” we eventually create another problem: a Service with hundreds or thousands of lines that owns validation, persistence, integration, conversion, and workflow.
A better question is:
What business capability does this Service represent?
For our example, the capability is clear:
public interface ReorderService{ ReorderResult reorder(String orderCode);}
The Service coordinates the reorder operation. It doesn’t have to implement every rule itself.
Platform: CartService, CommerceCartService, CommerceCartParameter, CommerceCartModification.
Custom: ReorderService.
SAP Commerce gives us a useful example in CommerceCartService. When we call:
commerceCartService.addToCart(parameter);
we’re asking the platform to perform a commerce operation.
Compare that with directly creating an AbstractOrderEntryModel and calling modelService.save(entry). Both may eventually change persisted data, but they communicate different intent.
One says, “perform the add-to-cart business operation.” The other says, “persist this model.”
Design Insight
Good Service APIs describe what the business is doing, not how the database is being manipulated.
6. Where Does Reorder Eligibility Belong?
Suppose the first rule is simple: an Order can be reordered only when its status is COMPLETED.
A focused method may be enough. There is no reason to create an interface, factory, registry, and multiple implementations for one stable condition.
Then the requirement grows. Reorder is allowed only when the Order is completed, within the allowed reorder period, belongs to an active account, uses a supported Order type, and still contains at least one orderable entry.
Still manageable.
But later, Contract Orders and Standard Orders have genuinely different eligibility policies. Now the variation itself has become part of the business.
This is where a Strategy can start earning its place:
public interface ReorderEligibilityStrategy{ boolean supports(OrderModel order); boolean isEligible(OrderModel order);}
The point isn’t that Strategy is more architectural than an if statement. The point is that we now have a business rule that varies independently from the main reorder flow.
Common Mistake
Don’t introduce a Strategy simply because a method contains an if/else. Introduce it when the business behavior genuinely varies or needs a clear extension point.
7. When Has Logic Earned Its Own Class?
Once developers understand separation of responsibilities, another problem can appear: we start separating everything.
A Service method gets longer, so we create a Helper. We see an if/else, so we create a Strategy. We have three checks, so we create a Validator. Then we add a Factory to choose the Strategy.
The purpose of another class is not to make the original class shorter. It is to give a separate responsibility a clear owner.
Start with a private method
If isWithinReorderPeriod(order) is used only inside ReorderService, has one stable implementation, and is not independently reusable, a private method may be the simplest correct design.
Design Insight
Don’t extract a class because a method exists. Extract a class when a responsibility has become independently meaningful.
When does a Validator make sense?
Create a focused Validator when related checks form a meaningful validation responsibility, the checks obscure the main Service flow, multiple callers need the same validation, or the rules benefit from independent tests.
A Validator answers: Is this operation/input valid?
When does a Strategy make sense?
Create a Strategy when behavior genuinely varies. SAP Commerce itself uses strategy-oriented extension points. In the 2211 commerce-services APIs, examples include OOTB interfaces such as CommerceAddToCartStrategy, CommerceCartCalculationStrategy, CommerceRemoveEntriesStrategy, and CommerceSaveCartStrategy.
Our ReorderEligibilityStrategy is custom, but the pattern is familiar in the platform.
A Strategy answers: Which behavior applies here?
Validator vs Strategy
If Contract Orders merely have an extra rule—“cannot reorder after 30 days”—a Validator may be enough. If Contract Orders follow a materially different reorder process with different quantity, pricing, or entry-handling behavior, a Strategy becomes more meaningful.
What is a Factory, and when do we need one?
A Factory creates or provides an appropriate object without forcing the caller to know the construction details.
But in a Spring/SAP Commerce application, Strategy beans are often already created by Spring. If our problem is selecting one existing Strategy from a list, ReorderStrategyResolver is often clearer than ReorderStrategyFactory.
public ReorderStrategy resolve(final OrderModel order){ return strategies.stream() .filter(strategy -> strategy.supports(order)) .findFirst() .orElseThrow( () -> new IllegalStateException( "No reorder strategy found"));}
The progression should usually be: start simple; introduce multiple Strategies when behavior genuinely varies; introduce a Resolver or Factory only when selection or construction becomes a responsibility of its own.
When Should We Create a Helper?
Helper is a common enterprise class name, but it can easily hide unclear responsibilities.
Imagine ReorderService grows and we move methods into:
public class ReorderHelper{ public OrderModel findOrder(...) { ... } public boolean validateOrder(...) { ... } public long calculateQuantity(...) { ... } public AvailabilityResult checkInventory(...) { ... } public ReorderData convertResult(...) { ... }}
The Service is shorter, but the Helper now owns persistence, validation, calculation, integration, and conversion. We moved the problem instead of separating responsibilities.
Compare that with precise names:
ReorderValidator— validates whether reorder can proceed.ReorderQuantityCalculator— calculates reorder quantity.ReorderOrderDao— retrieves Order data.InventoryClient— communicates with an external inventory API.ReorderResultConverter— transforms the result representation.
Before creating ReorderHelper, ask: Can I give this class a more precise responsibility-based name?
Common Mistake
Creating a Helper can make the original Service look cleaner without improving the architecture. Don’t create a Helper because you need somewhere to put methods; create another class because those methods together represent one responsibility.
Integration Service vs Adapter / Client
Suppose reorder needs current inventory from an external system. Calling that system directly from ReorderService means the business capability starts knowing endpoints, authentication, request/response formats, timeouts, and technical errors.
ReorderService ↓AvailabilityService ↓InventoryAdapter / Client ↓External Inventory System
Note: AvailabilityService and InventoryAdapter in this example are custom components introduced to illustrate the integration boundary; they are not SAP Commerce OOTB APIs.
Integration Service
The Integration Service speaks in terms the Commerce application understands:
public interface AvailabilityService{ AvailabilityResult getAvailability( ProductModel product, long requestedQuantity);}
It answers: What external capability does my application need? For example: get availability, retrieve pricing, get customer credit, or submit an Order.
Adapter / Client
The Adapter or Client sits closer to the external contract:
public interface InventoryClient{ InventoryResponse getInventory( InventoryRequest request);}
It may own endpoint configuration, authentication, headers, REST/OData/RFC details, external DTOs, serialization, and technical error handling.
It answers: How do I communicate with the system that provides this capability?
Do we always need both?
No. For a very small integration, Service → Adapter → Client may add layers without adding clarity. Start with the simplest boundary that keeps external-system details out of the business capability, then add another layer when it earns its place.
Design Insight
Integration Service and Adapter solve different problems. The Service expresses what the application needs; the Adapter or Client isolates how an external system provides it.
When Does a Converter or Populator Make Sense?
SAP Commerce provides the OOTB Populator<SOURCE, TARGET> pattern and populating-converter infrastructure for representation transformation.
Suppose the Service works with OrderModel, while the consumer needs OrderData:
OrderModel ↓Converter ├── BasicOrderPopulator ├── PricePopulator └── EntryPopulator ↓OrderData
What should a Populator do?
A Populator should primarily answer: How do I copy or transform this focused part of the source into the target?
public class ReorderBasicPopulator implements Populator<OrderModel, ReorderData>{ @Override public void populate( final OrderModel source, final ReorderData target) { target.setOrderCode(source.getCode()); if (source.getStatus() != null) { target.setStatus(source.getStatus().getCode()); } }}
Where Populators become dangerous
If a Populator checks Order status, customer state, inventory, and reorder policy before setting reorderAllowed, it is no longer just transforming representation; it is owning business behavior.
A clearer design is:
target.setReorderAllowed( reorderEligibilityStrategy.isEligible(source));
ReorderEligibilityStrategy owns the business decision. ReorderInfoPopulator maps that decision into the target.
Common Mistake
A Populator can become a hidden business Service because it is a convenient place to calculate target fields. Keep asking whether you’re transforming information or making a business decision.
Converter vs Populator
A useful mental model is:
- Converter: create or produce the target representation.
- Populator: populate a focused part of that target.
OrderModel ↓OrderData ↓OrderWsDTO ↓JSON
This lets persistence Models, application Data objects, and external API representations serve different consumers.
Design Insight
Use Converters and Populators to transform representations. Don’t use them as a convenient place to hide business rules.
Quick Decision Guide
| If you need to… | Think about |
|---|---|
| Group unrelated convenience methods | Don’t default to Helper |
| Calculate one focused business value | Calculator |
| Validate whether an operation is allowed | Validator |
| Support genuinely different behavior | Strategy |
| Choose the applicable Strategy | Resolver |
| Create/provide an object with non-trivial construction | Factory |
| Build a complex request/object step by step | Builder |
| Express an external business capability | Integration Service |
| Communicate with an external contract | Adapter / Client |
| Convert Model/domain result into Data | Converter |
| Populate a focused part of Data | Populator |
| Retrieve persistent data | DAO |
Design Insight
The question behind all of these choices is the same: What responsibility am I trying to give a clear owner?
A practical class-selection guide
| If the code… | Consider |
|---|---|
| Handles HTTP request/response | Controller |
| Exposes a consumer-friendly capability | Facade |
| Represents a business operation | Service |
| Checks whether an operation is allowed | Validator |
| Implements behavior that genuinely varies | Strategy |
| Selects among existing Strategies | Resolver |
| Creates/provides objects with non-trivial construction | Factory |
| Retrieves persistent data | DAO |
| Calculates one focused result | Calculator |
| Builds a complex object/request | Builder |
| Transforms one representation to another | Converter / Populator / Mapper |
| Talks to an external contract/protocol | Adapter / Client |
| Handles a specific event/case/step | Handler |
| Protects a ServiceLayer model invariant | Interceptor, when appropriate |
| Has no clear responsibility yet | Don’t create the class yet |
Five questions before extracting another class
- Does this code represent a separate responsibility?
- Will this behavior change independently?
- Is it reused or needed from another appropriate boundary?
- Does extraction make the business flow easier to read?
- Can I give the class a precise name?
Design Insight
If you can’t explain in one sentence why a new class exists, you probably aren’t ready to create it yet.
8. Validation: Form, Controller, Service, or Interceptor?
Validation is one of the easiest places to mix responsibilities because the word validation can mean several different things.
Before choosing a Validator, Service, or Interceptor, ask:
What exactly am I validating?
Storefront form validation
Suppose the reorder page collects a reference number and requested delivery date. Before executing the business operation, we may need to check required fields, length, and input format.
Those are input/form concerns. In a Spring MVC storefront, a custom Spring Validator can own them:
public class ReorderFormValidator implements Validator{ @Override public boolean supports(final Class<?> clazz) { return ReorderForm.class.equals(clazz); } @Override public void validate(final Object target, final Errors errors) { final ReorderForm form = (ReorderForm) target; ValidationUtils.rejectIfEmptyOrWhitespace( errors, "referenceNumber", "reorder.referenceNumber.required"); if (form.getReferenceNumber() != null && form.getReferenceNumber().length() > 50) { errors.rejectValue( "referenceNumber", "reorder.referenceNumber.invalidLength"); } }}
The Controller handles the validation result at the request boundary, for example through BindingResult.
Spring/framework: Validator, Errors, ValidationUtils, BindingResult.
Custom: ReorderForm, ReorderFormValidator.
Design Insight
Form validation should tell the user whether submitted input is acceptable. It should not become the only place where an important business rule is enforced.
Why business rules should not live only in the Form Validator
If ReorderFormValidator retrieves an Order, evaluates reorder eligibility, and calls inventory, the storefront may work—but OCC and CronJobs can bypass that form completely.
The rule “this Order is not eligible for reorder” still matters when no HTML form exists, so it belongs below the form boundary.
Business validation
public class ReorderValidator{ public void validate(final OrderModel order) { if (!OrderStatus.COMPLETED.equals(order.getStatus())) { throw new ReorderNotAllowedException( "Order is not eligible for reorder"); } if (!isWithinReorderPeriod(order)) { throw new ReorderNotAllowedException( "Reorder period has expired"); } }}
Platform: OrderModel, OrderStatus.
Custom: ReorderValidator, ReorderNotAllowedException.
Now Storefront, OCC, Backoffice, or scheduled processing can reuse the same business validation.
Model invariant
If a custom model must never be persisted with startDate > endDate regardless of whether the save originates from Storefront, OCC, Backoffice, ImpEx, or a CronJob, that is a different concern.
SAP Commerce provides the OOTB ValidateInterceptor<MODEL> lifecycle interface. Its onValidate(...) method runs in the ServiceLayer validation phase before persistence and can throw InterceptorException.
Platform: ValidateInterceptor, InterceptorContext, InterceptorException, ModelService.
Custom: your interceptor implementation for the specific model invariant.
Design Insight
Don’t ask only “Where should validation go?” Ask what you’re protecting: the submitted input, the business operation, or the persistent model state.
9. Where Should the FlexibleSearch Query Go?
Our ReorderService needs the original Order.
We could inject FlexibleSearchService directly into the Service and write the query there. It would work. But now the business capability also knows how its data is retrieved.
Platform: FlexibleSearchService, FlexibleSearchQuery, SearchResult, OrderModel, UserModel.
Custom: ReorderOrderDao.
A DAO gives us a cleaner persistence boundary:
public interface ReorderOrderDao{ Optional<OrderModel> findOrderForCodeAndUser( String orderCode, UserModel user);}
A simplified implementation might use:
private static final String FIND_ORDER = "SELECT {o.pk} " + "FROM {Order AS o} " + "WHERE {o.code} = ?orderCode " + "AND {o.user} = ?user";public Optional<OrderModel> findOrderForCodeAndUser( final String orderCode, final UserModel user){ final FlexibleSearchQuery query = new FlexibleSearchQuery(FIND_ORDER); query.addQueryParameter("orderCode", orderCode); query.addQueryParameter("user", user); final SearchResult<OrderModel> result = flexibleSearchService.search(query); return result.getResult().stream().findFirst();}
Notice what the DAO does not do.
It doesn’t decide whether the Order is eligible for reorder. It doesn’t check inventory. It doesn’t update the Cart.
It answers a persistence question:
Which Order matches these criteria?
The Service then answers the business question:
What are we allowed to do with that Order?
This is also where the FlexibleSearch principles from Part 3 become relevant: parameterization, expected result size, Search Restrictions, indexes, and production data volume still matter.
Design Insight
A DAO should retrieve data. It shouldn’t quietly become the place where business policy is decided.
10. Now the Business Adds Real-Time Inventory
The feature works until the next requirement arrives:
Before adding an Order entry to the Cart, check current inventory from an external system.
This is where service-layer designs often start becoming messy.
It’s easy to add the external call directly inside ReorderService:
final InventoryResponse response = inventoryClient.getInventory(product.getCode());
Again, it works.
But what does ReorderService now need to understand?
- endpoint details,
- authentication,
- REST/OData/RFC behavior,
- request mapping,
- response mapping,
- timeouts,
- technical errors.
None of those are reorder rules.
The business capability should be able to ask a simpler question:
Is the requested quantity currently available?
Custom: AvailabilityService and InventoryAdapter. There is no generic OOTB InventoryAdapter represented by this example.
That leads to a boundary such as:
public interface AvailabilityService{ AvailabilityResult getAvailability( ProductModel product, long requestedQuantity);}
The implementation can delegate protocol-specific communication to an adapter/client.
Design Insight
Business Services should speak the language of the business. Integration adapters should speak the language of external systems.
11. What Happens When Inventory Times Out?
This is where production behavior matters more than the happy-path diagram.
The adapter may receive a socket timeout, authentication failure, invalid response, or unavailable-system error.
But should ReorderService expose a low-level technical exception to the storefront?
Usually, that’s not the useful abstraction.
The integration layer may translate the technical problem into something the business operation can understand, for example:
AvailabilityTemporarilyUnavailableException
Then the reorder capability decides what that means.
Should the entire operation fail? Should the user be asked to retry? Is cached availability acceptable? Those are business decisions, not HTTP-client decisions.
Enterprise Insight
Don’t let technical integration exceptions silently become your business contract. Detect technical failures close to the integration boundary, then decide their business meaning at the appropriate layer.
12. Synchronous or Asynchronous?
Suppose the user cannot proceed unless current inventory is known. A synchronous availability check may be appropriate.
Now compare that with an Order that has already been placed and needs to be sent downstream for fulfillment.
Does the customer really need to keep the checkout request open until every downstream step completes?
Often, no.
Platform: BusinessProcessService and BusinessProcessModel are OOTB process-engine APIs; your process definition and custom process actions are implementation-specific.
SAP Commerce Business Process is one platform capability that can support longer-running business workflows when the use case genuinely represents a process with state, transitions, retries, or waiting behavior.
final OrderProcessModel process = businessProcessService.createProcess( "order-process-" + order.getCode(), "order-process");process.setOrder(order);modelService.save(process);businessProcessService.startProcess(process);
The important lesson isn’t “use Business Process for everything asynchronous.”
It’s to ask whether the user really needs the external result before the current operation can continue.
Common Mistake
Don’t make every external interaction synchronous simply because synchronous Java code is easier to write. At the same time, don’t introduce asynchronous processing unless the business flow can tolerate it.
13. Transactions: What Should Succeed or Fail Together?
Now imagine the original Order contains 15 entries.
Entries 1 through 11 are added successfully. Entry 12 fails.
What should happen?
There are at least two valid answers.
All-or-nothing reorder
The business may say:
Either the complete eligible Order is copied successfully, or the Cart should remain unchanged.
In that case, treating the operation as one consistency boundary may make sense.
Partial reorder
The business may instead say:
Add every valid Product and return a warning for entries that can no longer be reordered.
Now rolling back the whole operation because one Product failed may be wrong.
The transaction decision comes from the business semantics.
That’s more important than simply deciding that every Service method should have @Transactional.
SAP Commerce also exposes transaction APIs through de.hybris.platform.tx.Transaction, while Spring-managed code can use Spring transaction configuration where appropriate.
final Transaction transaction = Transaction.current();transaction.begin();try{ // related persistence operations transaction.commit();}catch (final RuntimeException ex){ transaction.rollback(); throw ex;}
The syntax isn’t the architecture.
The real question is:
Which state changes must succeed or fail together?
Be careful with external calls inside a transaction
If a transaction updates the Cart, calls a remote system, waits several seconds, then performs more database work, the transaction may remain open while the application waits on the network.
Under load, that can contribute to longer lock duration, contention, resource usage, and more complicated failure behavior.
This doesn’t mean an external call can never occur in transactional code. It means the boundary deserves deliberate design.
Enterprise Insight
Transaction problems often don’t appear with one developer and a small database. They appear under concurrent traffic, slower downstream systems, larger Carts, retries, and database contention.
14. What Happens When a CronJob Needs the Same Capability?
Later, the business asks for a scheduled process that needs part of the same business behavior.
A common shortcut is:
CronJob ↓StorefrontFacade ↓Service
Technically, that may work.
But why does a background process depend on a Storefront-oriented abstraction?
The Facade may exist to produce Data objects, handle presentation-oriented assumptions, or simplify Storefront consumption. A CronJob may need none of that.
Platform: the ServiceLayer CronJob framework provides JobPerformable, AbstractJobPerformable, and PerformResult. The concrete job and business Service in this example are custom.
If the scheduled process needs the business capability, a cleaner dependency may simply be:
public PerformResult perform(final FollowUpCronJobModel cronJob){ followUpService.processEligibleOrders(); return new PerformResult( CronJobResult.SUCCESS, CronJobStatus.FINISHED);}
The same principle applies to Backoffice actions and other backend entry points.
Design Insight
Reuse business capabilities, not delivery mechanisms. A CronJob shouldn’t depend on a Storefront Facade simply because that’s where the useful method happened to be written first.
15. Don’t Hide Business Rules in Populators
Platform: SAP Commerce provides Populator<SOURCE,TARGET> and populating-converter infrastructure. The reorder-specific Populator/Data classes in this example are custom.
Converters and Populators are another place where logic can quietly accumulate.
Suppose the UI needs a field:
reorderAllowed
It’s tempting to calculate the entire rule inside an OrderPopulator:
public void populate( final OrderModel source, final OrderData target){ if (OrderStatus.COMPLETED.equals(source.getStatus()) && isWithinReorderPeriod(source) && inventoryService.hasInventory(source)) { target.setReorderAllowed(true); }}
It works.
But now the Populator owns a business decision.
When OCC, Backoffice, or another Service needs the same decision, developers either call the Populator for a reason it wasn’t designed for or duplicate the rule.
A better design is for the business rule to live in the component that owns it, while the Populator maps the result:
target.setReorderAllowed( reorderEligibilityStrategy.isEligible(source));
Common Mistake
Populators are convenient places to calculate fields, which is exactly why business logic can become hidden there. Ask whether you’re transforming information or deciding business behavior.
16. Models, Data Objects, and OCC DTOs Are Different Boundaries
Another shortcut is to return an OrderModel directly from an API.
That couples an external contract to a persistence-oriented representation containing platform attributes and relationships.
A more typical boundary is:
OrderModel ↓OrderData ↓OrderWsDTO ↓JSON
For example, an OCC layer can obtain Data from a Facade and map it to a web-service DTO:
final OrderData orderData = orderFacade.getOrderDetailsForCode(orderCode);return dataMapper.map( orderData, OrderWsDTO.class);
That separation isn’t ceremony for its own sake.
It gives the persistence model, application representation, and external API contract room to evolve independently.
Design Insight
Persistence Models are designed for the platform. External contracts are designed for consumers. Don’t force one representation to serve every layer simply to avoid conversion.
17. Now Let’s Put the Reorder Design Together
We started with a Controller that knew almost everything about reorder.
After following the requirement as it grew, the responsibilities now look different:
- the Controller owns the delivery boundary,
- the Facade exposes a consumer-friendly operation,
ReorderServiceowns the business capability,- eligibility rules can live in a focused Strategy when variation justifies it,
- the DAO owns persistence-oriented retrieval,
AvailabilityServiceexpresses the inventory capability,- the integration adapter owns protocol-specific communication,
- Converters/Populators transform representations rather than owning business policy.

18. A Simple Test: Change the Entry Point
Tomorrow, the business says:
Expose reorder through OCC.
Do we rewrite eligibility?
No.
Do we rewrite Cart behavior?
No.
Do we duplicate inventory integration?
No.
We add the appropriate API boundary and reuse the business capability.
That’s a useful architecture test:
When the delivery mechanism changes, how much business behavior has to change with it?
19. Another Test: Change the Inventory Provider
Today, AvailabilityService may use one adapter and external provider.
Tomorrow, the provider changes.
If ReorderService still asks the same question—”is this quantity available?”—then the business capability doesn’t need to understand the new authentication mechanism, payload, or protocol.
That’s the value of the integration boundary.
20. When You’re Unsure Where Logic Belongs
Start with the business operation, then work outward.
| Question | Likely boundary |
|---|---|
| Is this about HTTP/request/response? | Controller |
| Is this about a consumer-friendly representation? | Facade / conversion |
| Is this a business decision or capability? | Service / focused business component |
| Is this a persistence query? | DAO |
| Is this an external protocol/provider detail? | Adapter / Client |
| Is this a model invariant across ServiceLayer saves? | Interceptor, when appropriate |
If you still cannot decide, don’t immediately create another layer. Ask:
Who should own this decision?
That question is usually more useful than asking which package the class should go into.
21. What I Would Look for During Code Review
When reviewing a SAP Commerce feature like this, I don’t start by checking whether every layer from an architecture diagram exists.
I look for responsibility leaks.
- Does the Controller execute FlexibleSearch or manipulate Models directly?
- Has the Facade become the real business Service?
- Is one Service responsible for every rule and integration?
- Does the DAO decide business eligibility?
- Are Populators calling external systems or making business decisions?
- Does a CronJob depend on a Storefront-specific Facade?
- Are remote calls happening inside long database transactions without a clear reason?
- Could OCC reuse the capability without copying logic?
None of these automatically proves the design is wrong.
They are signals that tell me where to ask questions.
Enterprise Insight
An application can contain Controller, Facade, Service, and DAO classes and still have poor architecture. The class names don’t create the separation. The responsibilities do.
22. Don’t Overengineer the Solution Either
There is another side to this discussion.
After learning about clean responsibility boundaries, it’s easy to create:
ReorderFacadeReorderServiceReorderValidatorReorderStrategyReorderStrategyFactoryReorderContextReorderProcessorReorderHandlerReorderCoordinator
for a requirement that contains 20 lines of meaningful behavior.
That isn’t automatically good architecture.
Every abstraction has a cost. Someone has to understand it, test it, navigate it, maintain it, and debug it.
Design Insight
Don’t design for imaginary complexity. Add structure when the responsibility or expected variation justifies it. Good architecture balances simplicity today with maintainability tomorrow.
23. A Practical Responsibility Guide
I wouldn’t treat the following as rigid rules, but they’re useful starting points during design and code review.
| Requirement | Typical home |
|---|---|
| Read HTTP parameters and build HTTP response | Controller |
| Expose a consumer-friendly operation | Facade |
| Coordinate the reorder business capability | Service |
| Implement genuinely variable eligibility policy | Strategy |
| Validate a business operation | Service / focused Validator |
| Protect a model invariant across ServiceLayer saves | Interceptor, when appropriate |
| Retrieve an Order using persistence criteria | DAO |
| Execute FlexibleSearch for that DAO | DAO / persistence component |
| Convert Model/domain result to Data | Converter / Populator |
| Express availability in business terms | Availability / integration-facing Service |
| Handle REST, OData, RFC, authentication, mapping | Adapter / Client |
| Expose the external API representation | OCC / WsDTO boundary |
The useful question isn’t:
“Which layer does our coding standard say this belongs in?”
It’s:
“Which component should own this responsibility, and why?”
24. Before You Move On
Think about one SAP Commerce feature you’ve worked on recently.
Maybe it involved Cart validation, pricing, an external integration, Order processing, a CronJob, or Backoffice behavior.
Ask yourself:
- Where does the actual business capability live?
- Could OCC or another entry point reuse it?
- Does the Controller know persistence details?
- Is the Facade doing the Service’s job?
- Has the Service become a dumping ground?
- Is the DAO deciding business policy?
- Are integration details leaking into business logic?
- Are business rules hidden inside Populators?
- Are transaction boundaries intentional?
You don’t need to redesign every existing feature.
The goal is to recognize these responsibility problems earlier, when they’re still inexpensive to fix.
25. Scenario Questions
Scenario 1 — FlexibleSearch in the Controller
A Controller retrieves an Order using FlexibleSearchService, checks ownership, updates the Cart, and returns the page.
What would you change first, and why?
Scenario 2 — The 1,500-Line Facade
The Controller is thin, but the Facade contains pricing rules, inventory validation, FlexibleSearch, integration calls, and conversion.
Did we really achieve separation of responsibilities?
Scenario 3 — CronJob Reuses a Storefront Facade
A CronJob needs functionality that already exists in a Storefront Facade.
Would you reuse the Facade, or should the reusable capability live lower?
Scenario 4 — Validation Needed From Every Entry Point
A rule must hold when a model is saved from OCC, Storefront, Backoffice, ImpEx, and a CronJob.
Is request validation enough, or are you looking at a model invariant?
Scenario 5 — External API Inside a Long Transaction
A Service updates several Models, calls a remote system that sometimes takes eight seconds, and then performs more database updates.
What would you investigate before deciding that the transaction boundary is correct?
26. Frequently Asked Questions
Should every Controller call a Facade?
Not as an absolute rule. The important principle is to keep delivery concerns separate from reusable business behavior. In many SAP Commerce storefront designs, a Facade provides a useful consumer-oriented boundary.
Should all business logic be inside one Service?
No. A Service can orchestrate a business capability while focused Strategies, Validators, other Services, platform services, and integration components own specialized responsibilities.
Should a DAO contain business validation?
Generally, the DAO should focus on persistence criteria. “Find this Order for this user” is a persistence question. “This Order cannot be reordered because the business status is invalid” is a business decision.
Should a CronJob call a Facade?
Not automatically. If the CronJob needs the underlying business capability rather than a presentation-oriented representation, the Service is often the more appropriate reusable boundary.
Where should FlexibleSearch live?
Typically behind an appropriate DAO or persistence component rather than directly in Controllers or presentation-oriented Facades. Part 3 covers the query-design and performance side of that decision in detail.
27. Key Takeaways
- “It works” doesn’t automatically mean the logic is in the right place.
- Keep delivery concerns separate from reusable business capabilities.
- Don’t solve a fat Controller by creating a fat Facade.
- Design Services around meaningful business operations, not persistence actions.
- Keep FlexibleSearch and persistence concerns behind appropriate data-access boundaries.
- Separate business-friendly integration capabilities from protocol-specific adapters.
- Place validation according to what you’re protecting: request, business operation, or model invariant.
- Let transaction boundaries follow business consistency requirements.
Final Thoughts
When developers first learn SAP Commerce architecture, we often teach:
Controller → Facade → Service → DAO
That’s useful.
But it’s only the beginning.
Good architecture isn’t created because every feature has four classes with those names.
It’s created when each component has a responsibility that another developer can understand.
The Controller handles the delivery boundary.
The Facade makes the capability easier for its consumer to use.
The Service represents the business capability.
Focused Strategies and Validators own rules when separating those rules provides real value.
The DAO owns persistence-oriented retrieval.
Converters and Populators transform representations.
Integration Services and adapters isolate external-system concerns.
Interceptors protect model lifecycle rules when the model lifecycle is genuinely the correct boundary.
And transaction boundaries protect the state changes that actually need to remain consistent.
None of these layers exists simply because an architecture diagram says it should.
Each one should earn its place.
Good architecture isn’t about adding more layers. It’s about putting the right responsibility in the right place.
What’s Coming in Part 6?
SAP Commerce OCC API Design — Designing Clean, Secure, and Maintainable Commerce APIs
In Part 6, we’ll continue from exactly where this article ends.
We’ll take the reusable business capability we’ve created and look at what changes when it is exposed through OCC:
- what belongs in an OCC Controller,
- where OCC-specific logic should stop,
- how Data objects and WsDTOs should be separated,
- how request validation and errors should be handled,
- how to avoid leaking internal platform details,
- and how to design APIs that can evolve without breaking consumers.
As with Part 5, the goal won’t be simply to create an endpoint.
We’ll focus on the design decisions that make that endpoint maintainable in a real SAP Commerce application.
Technical References
The platform API names and version-sensitive examples in this article were checked against SAP Commerce 2211-family SAP Help documentation before publication.
- SAP Help — CommerceCartService (2211)
- SAP Help — CartService (2211)
- SAP Help — FlexibleSearchService (2211)
- SAP Help — BusinessProcessService (2211)
- SAP Help — ServiceLayer CronJob package (2211)
- SAP Help — Define Converters and Populators (2211)
Part of the SAP Commerce Architect Series
Practical SAP Commerce learning focused not only on how the platform works, but on how to think through enterprise design decisions.
Learn. Architect. Build Better.

Leave a comment