Coding Standards and Automated Quality Gates for SAP Commerce
Naming conventions, layering rules, null and exception discipline for SAP Commerce code, and how to enforce all of it automatically with SonarQube quality profiles and the sonarcheck build target.
A standards document nobody enforces is a wish list. This guide covers both halves of making standards real on an SAP Commerce project: the conventions worth agreeing on (naming, layering, null discipline, exception handling) and the machinery that enforces them without human effort (SonarQube with the platform's own quality profiles, wired into the build).
The payoff is not aesthetic. On a platform where your code sits on top of thousands of framework classes, predictable naming and layering are what let a new developer, or you in eighteen months, find the custom logic among the generated and inherited bulk.
Naming Conventions#
Follow the platform's own patterns#
SAP Commerce ships with conventions already in force. Fighting them means every developer context-switches between two styles:
| Area | Convention |
|---|---|
| Extensions | Lower-case only, named by content, usually with a project prefix: acmecore, acmefacades, acmefulfilment |
| Packages | Indicate the content area (core, storefront, initialdata); interfaces and implementations live in separate packages (...order and ...order.impl) |
| Classes | UpperCamelCase, descriptive, English. Platform pattern: DefaultProductService implements ProductService |
| Beans | lowerCamelCase ids, with alias used to swap implementations |
Custom implementations#
When you extend or replace platform classes, the name should tell the reader three things: it is custom, what project owns it, and what it does. The convention that survives contact with real projects:
// New service interface, prefixed with the project name
public interface AcmeProductService extends ProductService { ... }
// Implementation: "Default" replaced by something meaningful, or at least "Acme"
public class AcmeStagedProductService
extends DefaultProductService
implements AcmeProductService { ... }
Keep overridden platform classes in a mirrored subpackage so the origin is traceable: de.hybris.platform.commercefacades.order.impl.DefaultCheckoutFacade is overridden by com.acme.commerce.commercefacades.order.impl.AcmeCheckoutFacade.
The rest of the family, with the suffix telling the reader what contract the class fulfils:
public class CategoryPageController { ... } // renders exactly one page
public class NavigationBarComponentController { ... } // matches its CMS component name
public class AccountUniqueValidateInterceptor { ... } // entity + purpose + interceptor type
public class TaskExpirationDateAttributeHandler { ... } // item + attribute + handler
public class ErpProductImportJobPerformable { ... } // the implementation behind ErpProductImportCronJob
public class AcmeB2BBudgetDao { ... } // FlexibleSearch lives here, nowhere else
DAO method names carry their parameters and cardinality: findByCode(String code), findUniqueByCodeAndCatalog(...). A caller should know from the signature whether to expect zero, one, or many results.
Layering Rules#
Develop against interfaces everywhere: every facade, service, and DAO gets one. This is not ceremony; it is what makes Spring overriding, mocking in unit tests, and implementation swaps possible.
The responsibilities are strict:
- Facades expose business functionality to controllers and OCC. They speak DTOs (
*Dataclasses) exclusively. A facade that returns a model has broken the containment that keeps persistence concerns out of the presentation layer, and it will eventually leak lazy-loading and serialization problems into the storefront. - Services own business logic and are the only layer that creates, updates, or deletes models. Together they are your project's internal API.
- DAOs encapsulate every FlexibleSearch query in the codebase. A query string inside a service or, worse, a controller is a review rejection. DAOs return empty collections rather than null, always.
- DTOs hold values. No logic, no models, no clever getters.
Null Discipline#
The platform's own services demonstrate the two acceptable patterns. For lookups where absence is a programming error or a data corruption, validate and throw:
public ProductModel getProductForCode(final String code)
{
validateParameterNotNull(code, "Parameter code must not be null");
final List<ProductModel> products = productDao.findProductsByCode(code);
validateIfSingleResult(products,
format("Product with code '%s' not found!", code),
format("Product code '%s' is not unique, %d products found!", code, products.size()));
return products.get(0);
}
For lookups where absence is a normal business case, return Optional and never throw. Exceptions in Java are expensive to construct (the stack trace capture dominates), so using them for expected no-result paths in hot code is a measurable performance mistake, not just a style one:
public Optional<OrderModel> findOrderById(final String id)
{
final List<OrderModel> result = doSearch(id);
return result.isEmpty() ? Optional.empty() : Optional.of(result.get(0));
}
// Call sites stay branch-free:
findOrderById(id).ifPresent(calculationService::calculate);
return findOrderById(id)
.map(OrderModel::getEntries)
.orElse(Collections.emptyList());
The three ground rules the whole team signs: methods never return null, parameters are validated at public API boundaries, and null checks on your own code's return values are a smell indicating rule one was broken somewhere.
Exception Handling in a Layered Platform#
- Runtime exceptions by default. Checked exceptions need a documented reason; they infect every signature up the stack.
- Handle exceptions close to their origin, and translate them at layer boundaries: the DAO layer surfaces
ModelNotFoundExceptionorAmbiguousIdentifierExceptionfrom FlexibleSearch; the service layer wraps them into business-meaningful exceptions; OCC controllers map those onto HTTP status codes. Always chain the original cause. - Never
catch (Exception e). It swallowsRuntimeExceptionand with it every programming error you wanted to hear about. - Log an exception exactly once. A stack trace that appears four times at four layers makes the real origin archaeology.
- Document what a public method throws with
@throwsJavadoc. Callers can only handle what they know about.
Formatting and the Repository#
Pick one formatter configuration, commit it, and make the IDE apply it:
- Eclipse: commit
org.eclipse.jdt.ui.prefsandorg.eclipse.jdt.core.prefswith the project. - IntelliJ: import the shared style or use the platform's IntelliJ template project.
- UTF-8 everywhere, enforced.
- Never auto-format XML files wholesale. A reformatted 2,000-line
*-items.xmlmakes the actual one-line change invisible in the diff, which defeats review.
Keep the repository clean of anything the build generates. The .gitignore for a CCv2-style repository excludes the platform itself (hybris/bin/platform, hybris/bin/modules), generated classes and models, hybris/data, hybris/log, hybris/temp, and every local.properties that carries developer-specific values.
Enforcement: SonarQube with the Platform Profiles#
Conventions on a wiki page decay. Conventions in a quality gate do not. SAP Commerce has shipped SonarQube quality profiles in the distribution since 6.6, and the build has a first-class target for running analysis.
Server setup#
Run a current SonarQube LTS (the Community Build works fine for this). Then import the platform's profiles:
- In SonarQube, create or restore a quality profile from
{COMMERCE_ROOT}/build-tools/sonarqube/, starting withjava-hybris-profile.xml. Profiles for other languages ship alongside it. - Set the imported profile as the default Java profile for your projects.
- Review the exclusion and technical-debt settings; the defaults are sane but your project structure may need path adjustments.
The platform profile matters because it encodes SAP Commerce specifics that a generic Java profile misses, and it establishes a shared baseline across every project that uses it.
Wiring the build#
Configuration goes in local.properties (CI) or your environment properties. Scan only your own extensions; analysing the platform's shipped code burns hours to produce findings you cannot act on:
sonar.projectName=acme-commerce
sonar.projectKey=acme-commerce
sonar.projectVersion=1.0
sonar.extensions=acmecore,acmefacades,acmestorefront,acmebackoffice
sonar.host.url=https://sonarqube.acme.internal
sonar.token=squ_xxxxxxxxxxxxxxxxxxxx
Use a token, not credentials; current SonarQube versions expect sonar.token (the old sonar.login property is deprecated). Then, from hybris/bin/platform with setantenv sourced:
ant sonarcheck
sonarcheck replaced the old sonar target back in 6.7; if you find ant sonar in a pipeline script, that pipeline has not been reviewed in a long time, which is its own finding.
The gate, not the dashboard#
A SonarQube instance nobody gates on is a very expensive wall clock. The value comes from three integration points:
- Pull request analysis. Every PR gets scanned; new blocker issues fail the check and block the merge. This enforces the standard where it is cheapest to fix, on code that has not merged yet.
- Quality gate on new code. Gate conditions on the new-code period (no new blockers, coverage on new code above the agreed floor) rather than absolute totals. Legacy debt is managed down over time; new debt is refused at the door.
- Trend review. Ten minutes in the sprint review looking at the debt and coverage trends. If the trend is wrong, the fix is a process conversation, not a heroic cleanup sprint.
In the IDE, before the commit#
SonarQube for IDE (the tool formerly named SonarLint) runs the same rules live in IntelliJ, Eclipse, and VS Code. In connected mode it pulls the quality profile from your server, so developers see exactly the findings CI will raise, while typing rather than twenty minutes after pushing. Make connected-mode setup part of developer onboarding; it converts the quality gate from a punishment mechanism into an autocomplete-speed feedback loop.
Adopting This on an Existing Project#
Do not try to fix five years of findings in one quarter. The sequence that works:
- Import the platform profile, run
ant sonarcheck, and accept the ugly baseline number without acting on it. - Turn on PR analysis with a gate on new code only. From this moment the codebase stops getting worse.
- Agree the team checklist (this document plus the code review playbook) and reference specific rules in review comments instead of arguing taste.
- Schedule debt paydown by module, prioritized by change frequency: cleaning a module nobody touches is effort spent where it earns nothing.
The standards in this guide are a starting point, not scripture. What matters is that your team agrees on one set, writes it down next to the code, and lets the machines enforce everything machines can enforce. Human review time is too valuable to spend on formatting and naming; spend it on design.