Service Layer Patterns in SAP Commerce: Services, Facades, Converters, and Interceptors Done Right
The patterns that keep custom code maintainable: service-and-facade layering, converters and populators, interceptors versus events, and injectable design.
Dr. Elena Kovács
SAP Commerce Platform Architect
Core platform architecture, Spring, extension design, performance tuning, clustering, and JDK upgrades.
Every custom SAP Commerce extension is a set of choices about where logic lives, and the estates that stay maintainable are the ones that made those choices consistently. Business logic in a JSP controller, a facade doing database work, an interceptor with a remote call, a converter with a side effect: each is a small violation that compounds into an estate nobody can safely change. The platform provides a clear pattern language (services, facades, DTOs with converters, interceptors, events, models) and the discipline is simply using each piece for its job. This guide is that pattern language, the boundaries between the layers, and the dependency-injection and testing habits that keep the whole thing honest.
The Layering: Facade, Service, Model#
Three layers, each with one responsibility, and the value is in respecting the boundaries:
- Facades orchestrate for a specific consumer (a storefront controller, an OCC endpoint). They call services, assemble DTOs, and speak the consumer's language. A facade contains no business logic and no persistence; it composes.
ProductFacade.getProductForCodecalls the product service, converts the model to a DTO, and returns it. - Services hold the business logic and the persistence orchestration. They call DAOs for data, apply rules, and work in models, never DTOs. This is where "how does pricing actually work" lives. Services are consumer-agnostic: the same
ProductServiceserves the storefront facade, the OCC facade, and a cronjob. - Models are the type-system items (the type system guide), the persistent domain objects. Business logic does not live on them (they are data); it lives in services that operate on them.
The single most common architectural smell is logic in the wrong layer: a facade doing a FlexibleSearch (persistence leaked up), a service returning a DTO (presentation leaked down), a controller computing a discount (business logic leaked into the web layer, invisible to the OCC consumer). The headless guide's API-first rule depends entirely on this discipline: if the logic is in the service, every consumer (JSP, OCC, cronjob) gets it; if it is in the controller, only that controller does.
DTOs, Converters, and Populators#
The storefront and OCC never expose models directly; they expose DTOs (data beans), and the conversion is a specific pattern:
- Converters turn a model into a DTO (or back). The platform's
AbstractPopulatingConverterproduces a target DTO by running a list of populators. - Populators each fill part of the DTO from the source model. One populator sets basic fields, another sets pricing, another sets stock, and they compose. This granularity is the point: to add a field to a product DTO, you add or extend a populator, without touching the others.
public class ProductStockPopulator implements Populator<ProductModel, ProductData> {
@Override
public void populate(final ProductModel source, final ProductData target) {
target.setStock(stockService.getStockLevelStatus(source, /* ... */));
}
}
Wired into the converter's populator list via Spring, so extending conversion is a configuration change plus a small class, not a rewrite. The discipline: populators are pure mappers, cheap and side-effect-free. A populator doing a remote call or a heavy query per item is an N+1 (the FlexibleSearch guide) hiding in the conversion layer, and it is a classic source of slow product pages.
Interceptors Versus Events: Reacting to Change#
Two mechanisms react to model changes, and choosing correctly matters:
- Interceptors run synchronously in the persistence flow:
PrepareInterceptor(before save, mutate the model),ValidateInterceptor(before save, reject invalid state),LoadInterceptor,RemoveInterceptor. They enforce invariants and derive fields within the transaction. Use them for "this must always be true about this item" logic: a validate interceptor that rejects a product with a negative price is correct, because it runs every time, transactionally, for every writer including imports. - Events run after the fact, potentially asynchronously: fire an event on order placement, and listeners react (send a notification, trigger a feed). Use them for reactions that should not block or fail the originating transaction (the events and webhooks guides).
The trap is using each for the other's job. An interceptor doing a remote call couples your save to an external system's availability and latency (and holds a transaction open across the network, the transactions guide's warning). An event trying to enforce an invariant fails to prevent the bad state, because the state is already committed by the time the event fires. Invariants are synchronous interceptors; reactions are events.
Dynamic Attributes and Their Cost#
Dynamic attributes compute a value at read time instead of persisting it, backed by an attribute handler bean. They are useful for derived values that should not be stored (a computed status, a formatted string), and they avoid stale persisted duplicates. But they carry costs worth knowing:
- They execute on every read, so a dynamic attribute doing real work (a query, a service call) is a performance hazard when read in bulk (a list page rendering the attribute per row). Keep handlers cheap.
- They do not persist, which means they are not queryable in FlexibleSearch and not indexable directly (the FlexibleSearch and Solr guides): you cannot filter on a dynamic attribute in a query, and search indexing needs the underlying data, not the computed value.
- They are a migration hazard: a dynamic attribute whose handler bean disappears throws on system update (the 6.x-to-2211 guide's orphaned-attribute errors).
Use them for genuinely derived, read-cheap, non-queryable values; reach for a persisted attribute (with an interceptor to maintain it) when you need to query, index, or expensively compute.
Dependency Injection and Testability#
The platform is Spring, and the service layer's maintainability rests on using it well:
- Inject dependencies, do not look them up. Constructor or setter injection of services and DAOs, wired in Spring XML (or annotations where the project uses them), so every collaborator is explicit and replaceable. The occasional
Registry.getApplicationContext().getBean(...)lookup is a testability killer and a coupling smell. - Program to interfaces. Services and DAOs behind interfaces let tests substitute mocks and let the platform's extension mechanism override implementations cleanly (the bean-override pattern that makes extensions composable).
- This is what makes unit testing possible. A service with injected, interface-typed dependencies is unit-testable with mocks (the testing strategy guide); a service that news-up collaborators or does static lookups can only be integration-tested, which is slower and more brittle. The code review and coding standards guides treat injectable, interface-based design as a baseline for exactly this reason.
The Patterns as a Whole#
A well-structured extension reads predictably: controllers and OCC endpoints stay thin and call facades; facades orchestrate and convert; services own business logic and work in models; DAOs own queries; converters and populators map models to DTOs; interceptors enforce invariants synchronously; events handle reactions asynchronously; dynamic attributes derive cheap non-queryable values; and everything is injected behind interfaces so it is testable and overridable. None of these patterns is exotic; the maintainability comes entirely from applying them consistently, so any developer opening any extension finds logic where the pattern says it should be.
Checklist#
- Facades orchestrate and convert only: no business logic, no persistence
- Services own business logic in models; consumer-agnostic; no DTOs
- Controllers and OCC endpoints thin; all logic behind services (headless-ready)
- DTO conversion via converters and granular, side-effect-free populators; no per-item N+1
- Invariants in synchronous interceptors (validate/prepare); reactions in events; neither doing the other's job
- No remote calls or heavy work inside interceptors (transaction safety)
- Dynamic attributes only for cheap, derived, non-queryable values; persisted attributes when you must query/index
- Dependencies injected behind interfaces; no static bean lookups; unit-testable with mocks
The service layer is where a SAP Commerce project's long-term maintainability is decided, one placement choice at a time. The patterns are simple and the platform hands them to you; the skill is the discipline of always putting the logic where it belongs, so the estate stays one a new developer can reason about instead of one that only its authors dare to change.