Thanks for taking the time to contribute! This document describes the conventions we follow so that the codebase stays consistent and reviews stay fast. Please read it before opening a pull request.
- Language
- Branching Strategy
- Commit Guidelines
- Pull Requests
- Code Style (Java)
- Testing
- Code Review Etiquette
- Write all code and comments in English.
- Project documentation is written in German. (This CONTRIBUTING guide is in English, following the convention of the platform it's hosted on.)
developcontains the latest state of development.mainreflects the current release.- Use
feature/<description>branches for new functionality (e.g.feature/add-user-authentication). - Use
fix/<description>branches for bug fixes in existing code (e.g.fix/login-button-not-working). - Use
hotfix/<description>branches for critical fixes againstmain(e.g.hotfix/security-patch-for-cve-2024-1234).
-
Write small, focused commits. Each commit should contain a single, logical change. Avoid bundling unrelated changes together.
-
Commit frequently. Frequent commits with clear messages make rollbacks and history easier to follow.
-
Write meaningful commit messages. The diff already shows what changed; the commit message should explain why. Write it so an outside developer can understand it without additional context.
# bad "Add method validate" # good "Validate feature configuration before enabling to prevent runtime errors in production" -
Write commit messages in English.
-
Use semantic versioning for tags. Follow SemVer for release tags (e.g.
v1.0.0,v2.3.1). -
Run a linter before committing or pushing (e.g. Checkstyle or PMD) to catch style issues and common bugs automatically.
- Keep PRs small. Large pull requests are hard to merge and either stall in review or get rubber-stamped without a real look.
- Provide context in the description. Explain the goal of the PR and how the change can be tested — it helps reviewers understand the code faster.
- Ensure CI is green before requesting review and before merging.
- Use descriptive PR titles. PR titles are often used to generate changelogs, so
avoid vague titles like "fix bug" or "refactoring". Prefer precise titles such as
fix: resolve memory leak in session management. - Prefer merge commits (
--no-ff) over squash when merging, so the full history of development steps stays traceable. Squashing is acceptable for hotfixes or small changes to keep the history clean.
-
Use
camelCasefor variable and method names,PascalCasefor class and enum names, and tabs for indentation. -
Use descriptive names for variables and methods — verbose, speaking names help the reader understand code faster and should reveal what a method does, not how.
-
Use explicit typing; avoid overusing
var. Only usevarwhen the type is already obvious from the right-hand side (e.g.var client = new HttpClientFactory()); spell out the type when it comes from a method call or generic expression whose return type isn't visible at the call site.// bad var result = repository.find(id); // good Optional<Repository> result = repository.find(id);
-
Use named lambda parameters instead of single letters, especially in nested streams.
// bad users.stream().filter(u -> u.isActive()).forEach(u -> u.sendNotification()); // good users.stream() .filter(user -> user.isActive()) .forEach(activeUser -> activeUser.sendNotification());
-
Use
Optional<T>only for genuinely optional values — reserve it for values that are legitimately absent (e.g. a lookup that may find nothing), and useObjects.requireNonNull()/ fail-fast validation for values that must always be present. Don't wrap required fields inOptionaljust to avoid a null check. Once a value is anOptional<T>, unwrap it withorElse/orElseGet(or a ternary for plain nullable references) rather than calling.get()behind a null check.// bad (address must always be present) String zip = user.getAddress() == null ? null : user.getAddress().getZipCode(); // good (address is genuinely optional) Optional<Address> address = user.getAddress(); String zip = address.map(Address::getZipCode).orElse(null);
// bad String name = user.getName() != null ? user.getName() : "Default"; // good (Optional-based) String name = Optional.ofNullable(user.getName()).orElse("Default");
-
Prefer builders over long constructors once a type has more than 2-3 fields, so call sites read like named arguments (we use Lombok's
@Builder). Whenever a fluent chain (builder or otherwise) exceeds 2-3 calls, put each call on its own line for readability.// bad Config config = new Config(host, port, true, false, null, retries); config.setHost("localhost").setPort(8080).setEnabled(true).setDebug(false); // good Config config = Config.builder() .host(host) .port(port) .enabled(true) .build(); config.setHost("localhost") .setPort(8080) .setEnabled(true) .setDebug(false);
-
Use comments to explain why code does something, not what it does. What the code does should already be self-explanatory.
// bad // set retry to 3 int retryCount = 3; // good // we use 3 retries because the external API is unstable int retryCount = 3;
-
Fail fast and use defensive programming — validate inputs up front and return safe defaults instead of propagating
null.// fail fast void processOrder(Order order) { if (order == null) { throw new IllegalArgumentException("Order must not be null"); } // ... logic } // defensive programming List<String> getTags(User user) { if (user.getTags() == null) { return List.of(); } return user.getTags(); }
-
Keep classes and methods small and focused, following the single responsibility principle.
// bad class OrderManager { void processOrder(Order order) { /* ... */ } void sendEmail(String recipient, String message) { /* ... */ } void saveToDatabase(Order order) { /* ... */ } } // good class OrderService { void processOrder(Order order) { /* ... */ } } class EmailService { void sendEmail(String recipient, String message) { /* ... */ } }
-
Use the right exceptions. Prefer specific exceptions over the generic
RuntimeException/Exception, and create custom exception classes where the context calls for it.// bad throw new RuntimeException("Order not found"); // good throw new OrderNotFoundException("Order with ID " + orderId + " not found");
-
Avoid deeply nested code. Use guard clauses and fail-fast to keep nesting depth low.
// bad void process(User user) { if (user != null) { if (user.isActive()) { // ... a lot of logic } } } // good void process(User user) { if (user == null || !user.isActive()) { return; } // ... a lot of logic }
-
Obey the boy scout rule: "Always leave the campground cleaner than you found it." When you touch a file, fix small messes (typos, formatting) in the immediate area of your change.
-
Prefer text blocks /
String.formatover ad hoc concatenation when building strings — plain literals for static text,String.format(...)or text blocks ("""...""", Java 15+) when you actually need to build a string from parts.// good String constant = "I am a static string"; String dynamic = String.format("I am dynamic: %s", constant);
-
Avoid runtime metaprogramming and reflection. Reflection-based frameworks and dynamic proxies bypass compile-time type checking, hurt performance, and are fragile at runtime. Prefer direct API calls, interfaces, or composition/polymorphism.
// bad Method method = UserService.class.getDeclaredMethod("doSomething"); method.invoke(userService); // good userService.doSomething();
-
Use
@Slf4jfor logging. Lombok's@Slf4jannotation injects aprivate static final Logger logfield — don't hand-declare aLoggerviaLoggerFactory.getLogger(...)for a class's own logging. (A deliberately-named, cross-cutting logger not tied to the enclosing class name is a legitimate exception, since@Slf4jcan only produce a logger named after the class.)// bad import org.slf4j.Logger; import org.slf4j.LoggerFactory; class UserService { private static final Logger log = LoggerFactory.getLogger(UserService.class); } // good import lombok.extern.slf4j.Slf4j; @Slf4j class UserService { void doSomething() { log.info("Doing something..."); } }
-
Use uniform logging levels, consistently and deliberately, to keep log volume and readability sane in production:
debug/trace: detailed diagnostic info for development-time troubleshooting (e.g. method parameters, loop iterations).info: important, business-critical or systemic milestones (e.g. successful startup, completed transaction). Don't overuse.warn: unexpected situations that don't block the flow (e.g. fallbacks, use of deprecated APIs, transient connection errors).error: errors that require aborting or manual intervention (e.g. caught exceptions, system failures).
We use JUnit 5 and Mockito.
- Use descriptive test class names (e.g.
UserServiceTest). - Structure tests with given-when-then / arrange-act-assert comments.
- Use
@ParameterizedTest(with@ValueSource,@CsvSourceor@MethodSource) for data-driven tests. - Mock external dependencies using Mockito's
@Mock/Mockito.mock(...).
class CalculatorTest {
private final Calculator calculator = new Calculator();
@ParameterizedTest
@CsvSource({
"5, 10, 15",
"0, 0, 0",
"-3, 3, 0"
})
void shouldCalculateSumCorrectly(int a, int b, int expected) {
// when
int result = calculator.add(a, b);
// then
assertEquals(expected, result);
}
}@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@Test
void shouldThrowWhenOrderNotFound() {
// given
when(orderRepository.findById("123")).thenReturn(Optional.empty());
// when / then
assertThrows(OrderNotFoundException.class, () -> orderService.getOrder("123"));
}
}- Be constructive and respectful. Criticize the code, not the author. Phrase suggestions as questions or ideas (e.g. "Have you considered...?" instead of "This is wrong").
- Praise good code. If you see a particularly elegant solution, say so — reviews are also a place to learn and to give positive feedback.