Java Development Standards
June 24, 2026 · View on GitHub
Runtime vs Syntax Compatibility
- Runtime Environment: Java 21 (production)
- Syntax Requirement: Java 11 compatible (core modules)
- CLI Tools Exception: Java 21 features allowed in
tools/dotcms-clionly
✅ Use Java 11 Syntax in Core Modules
// Java 11 compatible patterns
var users = userAPI.findActiveUsers();
var contentTypes = contentTypeAPI.findAll();
// Java 11 compatible Optional and Stream operations
Optional<String> value = getValue();
String result = value.orElse("default");
List<String> names = users.stream()
.map(User::getName)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// Traditional string concatenation or String.format()
String query = "SELECT c.identifier, c.title FROM contentlet c " +
"WHERE c.structure_inode = ?";
// Traditional switch statements
String status;
switch (contentlet.getBaseType()) {
case CONTENT:
status = "Content";
break;
case HTMLPAGE:
status = "Page";
break;
default:
status = "Unknown";
}
✅ Java 21 Syntax (CLI/Tools Modules Only)
// ONLY in tools/dotcms-cli and test modules
var query = """
SELECT c.identifier, c.title FROM contentlet c
WHERE c.structure_inode = ?
""";
var status = switch (contentlet.getBaseType()) {
case CONTENT -> "Content";
case HTMLPAGE -> "Page";
default -> "Unknown";
};
public record UserInfo(String id, String email, String name) {}
Core Development Patterns
API Locator Pattern (Required)
// Service access - ALWAYS use APILocator
UserAPI userAPI = APILocator.getUserAPI();
ContentletAPI contentletAPI = APILocator.getContentletAPI();
// Web services
UserWebAPI userWebAPI = WebAPILocator.getUserWebAPI();
// ❌ NEVER instantiate services directly
// UserAPIImpl userAPI = new UserAPIImpl();
Configuration Management (Required)
Security: All configuration must follow Security Principles
import com.dotmarketing.util.Config;
// Hierarchical naming for new properties
boolean enabled = Config.getBooleanProperty("experiments.enabled", false);
String url = Config.getStringProperty("experiments.auto-js-injection.url", "");
// Environment variables automatically get DOT_ prefix
// experiments.enabled → DOT_EXPERIMENTS_ENABLED
Property Resolution Order
- Environment variables with
DOT_prefix (e.g.,DOT_EXPERIMENTS_ENABLED) - System table for both transformed and original names
- Properties files for both transformed and original names
New Property Naming Convention
# Use hierarchical domain-driven naming (RECOMMENDED)
experiments.enabled=true
experiments.auto-js-injection.enabled=true
experiments.auto-js-injection.url=https://example.com/script.js
experiments.auto-js-injection.max-retries=3
health.checks.database.timeout-seconds=30
health.monitoring.include-system-details=true
Logging Standards (Required)
import com.dotmarketing.util.Logger;
Logger.info(this, "Operation completed successfully");
Logger.error(this, "Operation failed: " + error.getMessage(), error);
// ❌ NEVER use: System.out.println(), printStackTrace(), System.err.println()
Immutable Objects (Critical Pattern)
@Value.Immutable
@JsonSerialize(as = ImmutableMyEntity.class)
@JsonDeserialize(as = ImmutableMyEntity.class)
public abstract class MyEntity {
public abstract String name();
public abstract Optional<String> description();
@Value.Default
public boolean enabled() { return true; }
public static Builder builder() { return ImmutableMyEntity.builder(); }
}
// Usage: MyEntity.builder().name("test").description("desc").build()
// IMPORTANT: Run ./mvnw compile after creating @Value.Immutable classes
REST view objects use a different style. The example above generates
Immutable*-prefixed types. REST response views instead use theAbstract*interface style (@Value.Style(typeAbstract = "Abstract*")) so the generated type drops the prefix, pluspassAnnotations = Schema.classfor Swagger. See REST_API_PATTERNS.md → View Object Pattern.
Exception Handling (dotCMS Hierarchy)
try {
riskyOperation();
} catch (SQLException e) {
Logger.error(this, "Database operation failed: " + e.getMessage(), e);
throw new DotDataException("Failed to process request", e);
} catch (SecurityException e) {
throw new DotSecurityException("Access denied", e);
}
// Exception types: DotDataException, DotSecurityException, DotRuntimeException, DotStateException
Utility Methods (Null-Safe Patterns)
import com.dotmarketing.util.UtilMethods;
// ALWAYS use UtilMethods.isSet() for null checking
if (UtilMethods.isSet(myString)) { // checks null, empty, and "null" string
processString(myString);
}
// Collections
List<String> list = CollectionsUtils.list("item1", "item2");
Map<String, Object> map = CollectionsUtils.map("key1", "value1", "key2", "value2");
// Safe supplier pattern (avoids NullPointerException)
String value = UtilMethods.isSet(() -> complex.getObject().getValue())
? complex.getObject().getValue()
: "default";
Database Access Patterns
import com.dotmarketing.common.db.DotConnect;
// Query with parameters
DotConnect dotConnect = new DotConnect();
List<Map<String, Object>> results = dotConnect
.setSQL("SELECT * FROM my_table WHERE column1 = ? AND column2 = ?")
.addParam("value1")
.addParam("value2")
.loadResults();
// Use with LocalTransaction for atomic operations
LocalTransaction.wrapReturn(() -> {
return dotConnect.setSQL("UPDATE my_table SET column1 = ?")
.addParam("newValue")
.executeUpdate();
});
Permission Checks — Batch vs Scalar
Always prefer the batch overload when filtering a collection. The scalar doesUserHavePermission(Permissionable, int, User, boolean) runs one DB/cache lookup per item; on a list of N items that is N round-trips (the N+1 problem). The batch overload resolves the entire collection in a single SQL UNION query.
// ❌ N+1 — one DB lookup per folder
for (Folder folder : folders) {
if (permissionAPI.doesUserHavePermission(folder, PermissionAPI.PERMISSION_READ, user, false)) {
visible.add(folder);
}
}
// ✅ Batch — one SQL round-trip for the whole collection
List<Folder> visible = permissionAPI.filterCollection(
folders, PermissionAPI.PERMISSION_READ, user, false);
Signature (PermissionAPI):
// Batch overload — uses getPermittedIds() in PermissionBitFactoryImpl under the hood
<P extends Permissionable> List<P> filterCollection(
Collection<P> permissionables,
int permissionType,
User user,
boolean respectFrontendRoles) throws DotDataException, DotSecurityException;
Notes:
- Admin/system-user fast-path returns the full collection immediately (no SQL).
respectFrontendRoles=falseexcludes anonymous and front-end roles from the role set.- The existing
filterCollection(List, int, boolean, User)overload is still available but does N+1 internally — prefer the batch overload for any new code that filters collections. - Implementation:
PermissionBitAPIImpl+PermissionBitFactoryImpl.getPermittedIds().
CDI Patterns (For New Components)
@ApplicationScoped
public class MyService {
private final JobQueueManagerAPI jobQueueManagerAPI;
// Default constructor required for CDI proxy
public MyService() {
this.jobQueueManagerAPI = null;
}
@Inject
public MyService(JobQueueManagerAPI jobQueueManagerAPI) {
this.jobQueueManagerAPI = jobQueueManagerAPI;
}
}
// Safe CDI bean access
Optional<MyService> service = CDIUtils.getBean(MyService.class);
MyService service = CDIUtils.getBeanThrows(MyService.class);
Build Integration Requirements
After Code Changes
- Immutable classes: Run
./mvnw compileafter@Value.Immutablechanges - Fast iteration:
./mvnw install -pl :dotcms-core -DskipTests - Docker updates: Run
./mvnw clean install(without-Ddocker.skip)
Critical Docker Build Workflow
Docker image updates only happen when building WITHOUT -Ddocker.skip:
# Fast development cycle (no Docker image update)
./mvnw install -pl :dotcms-core -DskipTests -Ddocker.skip
# When ready to test in Docker (REQUIRED for new servlets/endpoints):
./mvnw -DskipTests clean install # Updates Docker image
./mvnw -pl :dotcms-core -Pdocker-start -Dtomcat.port=8080
Legacy Patterns to Avoid in New Code
See: Progressive Enhancement for safe improvement strategies
// ❌ Avoid in new development
@Deprecated public class MyPortletAction extends PortletAction {}
@Deprecated public class MyAjax extends WfBaseAction {}
System.out.println("message");
System.getProperty("property");
StructureAPI structureAPI = APILocator.getStructureAPI();
// ✅ Use modern alternatives
@Path("/v1/resource") public class MyResource {}
Logger.info(this, "message");
Config.getStringProperty("property", "default");
ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI();
// ✅ For class metadata analysis, prefer Jandex over reflection
List<Class<?>> annotatedClasses = JandexClassMetadataScanner.findClassesWithAnnotation(
MyAnnotation.class, "com.dotcms.mypackage");
// See: docs/backend/JANDEX_METADATA_SCANNING.md