Server Development
General Best Practices
- Always use the least possible access level; prefer
privateoverpublic(package-private or protected can be used as well). - Avoid using
@Transactionalrandomly. Transactions can hurt performance, introduce locking/concurrency issues, and add complexity. See: https://codete.com/blog/5-common-spring-transactional-pitfalls/ - Define a constant if the same value is used more than once. This makes future changes easier.
- Facilitate code reuse. Move duplicated code to reusable methods. Use Generics where appropriate. IntelliJ can help find and extract duplicates.
- Always qualify a static class member reference with its class name, not with a reference or expression of that class's type.
- Prefer primitive types to classes (e.g.
longinstead ofLong). - Use
./gradlew spotlessCheckand./gradlew spotlessApplyto check and fix Java code style. - Don't use
.collect(Collectors.toList()). Use.toList()for an unmodifiable list or.collect(Collectors.toCollection(ArrayList::new))for a new ArrayList.
Project structure & Naming
Folder structure
The main application is stored under /src/main and separated into modules:
- resources - scripts, config files and templates.
- config - different configurations (production, development, etc.) for application.
- liquibase - contains
master.xmlfile where all the changelogs from the changelog folder are specified. When you want to do changes to the database, you will need to add a new changelog file here. To understand how to create a new changelog file you can check existing changelog files or read documentation: https://www.liquibase.org/documentation/databasechangelog.html.
- liquibase - contains
- config - different configurations (production, development, etc.) for application.
- java - Artemis Spring Boot application is located here. It contains the following folders:
- config - different classes for configuring database, Sentry, Liquibase, etc.
- domain - all the entities and data classes are located here (the model of the server application).
- dto - contains Data Transfer Objects (DTOs) that are used to transfer data between the server and the client.
- exception - store custom types of exceptions here. We encourage it to create custom exceptions to help other developers understand what problem exactly happened. This can also be helpful when we want to provide specific exception handling logic.
- repository - used to access or change objects in the database. There are several techniques to query a database: named queries, queries with SpEL expressions and Entity Graphs.
- security - contains different POJOs (simple classes that don't implement/extend any interface/class and don't have annotations) and component classes related to security.
- service - represents the controller of the server application. Add the application logic here. Retrieve and change objects using repositories.
- web - contains REST and websocket controllers that act as the view of the server application. Validate input and security here but do not include complex application logic. For websockets, use the
MessagingTemplateto push small data to the client or to notify the client about events.
Naming convention
All methods and classes should use camelCase style. The only difference: the first letter of any class should be capitalized. Most importantly, use intention-revealing, pronounceable names.
Variable names should also use camelCase style, where the first letter should be lowercase. For constants, i.e. arguments with the static final keywords, use all uppercase letters with underscores to separate words: SCREAMING_SNAKE_CASE.
The only exception to this rule is for the logger, which should be named log.
Variable and constant names should also be intention-revealing and pronounceable.
Example:
public class ExampleClass {
private static final Logger log = LoggerFactory.getLogger(ExampleClass.class);
private static final int MAXIMUM_NUMBER_OF_STUDENTS = 10;
private final ExampleService exampleService;
public void exampleMethod() {
int numberOfStudents = 0;
[...]
}
}
Code Organization & Principles
Single responsibility principle
One class and one method should be responsible for only one action, it should do it well and do nothing else. Reduce coupling, if the method does two or three different things at a time then we should consider splitting the functionality.
Small methods
There is no standard pattern for method length among the developers. Someone can say 5, in some cases even 20 lines of code is okay. Just try to make methods as small as possible.
Duplication
Avoid code duplication. If we cannot reuse a method elsewhere, then the method is probably bad and we should consider a better way to write this method. Use Abstraction to abstract common things in one place.
Variables and methods declaration
- Encapsulate the code you feel might change in the future.
- Make variables and methods private by default and increase access step by step by changing them from a private to package-private or protected first and not public right away.
- Classes, methods or functions should be open for extension and closed for modification (open-closed design principle).
- Program for the interface and not for implementation, you should use an interface type on variables, return types of a method or argument type of methods. Just like using SuperClass type to store an object rather using SubClass.
- The use of interface is to facilitate polymorphism, a client should not implement an interface method if it's not needed.
- Type inference of variables - var vs. actual type:
- Variables with primitive types like int, long, or also String should be defined with the actual type by default.
- Types which share similar functionality but require different handling should also be explicitly stated, e.g. Lists and Sets.
- Variable types which are untypically long and would decrease readability when writing can be shortened with
var(e.g. custom DTOs).
Structure your code correctly
- Default packages are not allowed. It can cause particular problems for Spring Boot applications that use the
@ComponentScan,@EntityScanor@SpringBootApplicationannotations since every class from every jar is read. - All variables in the class should be declared at the top of the class.
- If a variable is used only in one method, then it would be better to declare it as a local variable of this method.
- Methods should be declared in the same order as they are used (from top to bottom).
- More important methods should be declared at the top of a class and minor methods at the end.
Comments
- Add Javadoc and inline comments to clarify code and intent.
- Use AI tools to assist but always review for accuracy.
- Comments should be in English and add value.
Keep it simple and stupid
- Don't write complex code.
- Don't write code when you are tired or in a bad mood.
- Optimization vs. Readability: always write code that is simple to read and which will be understandable for developers. Because the time and resources spent on hard-to-read code cost much more than what we gain through optimization.
- Commit messages should describe both what the commit changes and how it does it.
- ARCHITECTURE FIRST: writing code without thinking of the system's architecture is useless, in the same way as dreaming about your desires without a plan of achieving them.
Database & Persistence
Database
- Write performant queries that can also deal with more than 1000 objects in a reasonable time.
- Prefer one query that fetches additional data instead of many small queries, but don't overdo it. A good rule of thumb is to query not more than 3 associations at the same time.
- Think about lazy vs. eager fetching when modeling the data types. Generally avoid
fetch = FetchType.EAGER. - Do NOT use nested queries, because those have a bad performance, in particular for many objects.
- Simple datatypes: immediately think about whether
nullshould be supported as additional state or not. In most cases it is preferable to avoidnull. - Use
Datetimeinstead ofTimestamp.Datetimeoccupies more storage space compared toTimestamp, however, it covers a greater date range that justifies its use in the long run. Always usedatetime(3)
For detailed database guidelines, please refer to the Database Guidelines page.
File handling
- Never use operating system (OS) specific file paths such as "test/test". Always use OS independent paths.
- Do not deal with
File.separatormanually. Instead, use the Path.of(firstPart, secondPart, ...) method which deals with separators automatically. - Existing paths can easily be appended with a new folder using
existingPath.resolve(subfolder)
Proper annotation of SQL query parameters
Query parameters for SQL must be annotated with @Param("variable")!
Do not write
@Query("""
SELECT r
FROM Result r
LEFT JOIN FETCH r.feedbacks
WHERE r.id = :resultId
""")
Optional<Result> findByIdWithEagerFeedbacks(Long resultId);
but instead annotate the parameter with @Param:
@Query("""
SELECT r
FROM Result r
LEFT JOIN FETCH r.feedbacks
WHERE r.id = :resultId
""")
Optional<Result> findByIdWithEagerFeedbacks(@Param("resultId") Long resultId);
The string name inside must match the name of the variable exactly!
SQL statement formatting
We prefer to write SQL statements all in the upper case. Split queries onto multiple lines using the Java Text Blocks notation (triple quotation mark):
@Query("""
SELECT r
FROM Result r
LEFT JOIN FETCH r.feedbacks
WHERE r.id = :resultId
""")
Optional<Result> findByIdWithEagerFeedbacks(@Param("resultId") Long resultId);
Do NOT use Sub-queries
SQL statements which do not contain sub-queries are preferable as they are more readable and have a better performance. So instead of:
@Query("""
SELECT COUNT (DISTINCT p)
FROM StudentParticipation p
WHERE p.exercise.id = :exerciseId
AND EXISTS (SELECT s
FROM Submission s
WHERE s.participation.id = p.id
AND s.submitted = TRUE
)
""")
long countByExerciseIdSubmitted(@Param("exerciseId") long exerciseId);
you should use:
@Query("""
SELECT COUNT (DISTINCT p)
FROM StudentParticipation p
JOIN p.submissions s
WHERE p.exercise.id = :exerciseId
AND s.submitted = TRUE
""")
long countByExerciseIdSubmitted(@Param("exerciseId") long exerciseId);
Functionally, both queries extract the same result set, but the first one is less efficient as the sub-query is calculated for each StudentParticipation.
Criteria Builder
The Criteria Builder is a powerful feature in JPA/Hibernate that allows you to construct type-safe, dynamic queries in Java code, rather than using string-based JPQL or SQL. Use Criteria Builder when you need to build queries dynamically at runtime, require type safety, or want to avoid hardcoding query strings. It is especially useful for complex search/filtering scenarios where the query structure depends on user input or other runtime conditions.
For more details, please visit the Criteria Builder Guidelines page.
Utility & Configuration
Utility
Utility methods can and should be placed in a class named for specific functionality, not "miscellaneous stuff related to project". Most of the time, our static methods belong in a related class.
Auto configuration
Spring Boot favors Java-based configuration.
Although it is possible to use Spring Boot with XML sources, it is generally not recommended.
You don't have to put all your @Configuration into a single class.
The @Import annotation can be used to import additional configuration classes.
One of the flagship features of Spring Boot is its use of Auto-configuration. This is the part of Spring Boot that makes your code simply work.
It gets activated when a particular jar file is detected on the classpath. The simplest way to make use of it is to rely on the Spring Boot Starters.
Dependency injection
- Some of you may argue with this, but by favoring constructor injection, you can keep your business logic free from Spring. Not only is the @Autowired annotation optional on constructors, you also get the benefit of being able to easily instantiate your bean without Spring.
- Use setter-based DI only for optional dependencies.
- Avoid circular dependencies, try constructor and setter-based DI for such cases.
REST API & Controllers
For comprehensive REST API guidelines including controller best practices, route naming conventions, DTOs, and authorization, please see the REST API Guidelines page.
Key points:
- RestControllers should be stateless and delegate business logic to
@Serviceclasses - Always use DTOs for request and response bodies (never expose entities directly)
- Use appropriate authorization annotations (
@EnforceAtLeastInstructor,@EnforceRoleInResource, etc.) - Follow route naming conventions (kebab-case, proper structure)
Service & Dependency Best Practices
Avoid service dependencies
In order to achieve low coupling and high cohesion, services should have as few dependencies on other services as possible:
- Avoid cyclic and redirectional dependencies
- Do not break the dependency cycle manually or by using
@Lazy - Move simple service methods into the repository as
defaultmethods
Circular dependency resolution
Using @Lazy on constructor parameters or ObjectProvider<T> to work around circular dependencies is strictly forbidden. These patterns hide architectural problems and make the codebase harder to understand and maintain.
Forbidden patterns:
// BAD: Using @Lazy on parameter to break circular dependency
public MyService(@Lazy OtherService otherService) { ... }
// BAD: Using ObjectProvider to defer resolution
public MyService(ObjectProvider<OtherService> otherServiceProvider) { ... }
Why these patterns are problematic:
- They hide the underlying architectural issue instead of fixing it
- They make dependency graphs harder to understand
- They can lead to runtime errors instead of startup-time errors
- They encourage poor design decisions
How to properly resolve circular dependencies:
- Refactor the architecture - Extract shared logic into a new service that both services can depend on
- Move logic to repositories - Simple database operations can be
defaultmethods in repositories - Reconsider the design - A circular dependency often indicates that responsibilities are not properly separated
Exception: JPA entity listeners (classes using @PostPersist, @PostUpdate, @PreRemove) are instantiated by Hibernate during EntityManagerFactory construction, before the full Spring context is available. In this specific case, @Lazy on constructor parameters is the only viable solution. This exception is documented in the architecture tests.
An example for a simple method is finding a single entity by ID:
default Course findByIdWithLecturesElseThrow(long courseId) {
return getValueElseThrow(findWithEagerLecturesById(courseId), courseId);
}
This approach has several benefits:
- Repositories don't have further dependencies (they are facades for the database), therefore there are no cycles
- We don't need to check for an
EntityNotFoundExceptionin the service since we throw in the repository already - The "ElseThrow" suffix at the end of the method name makes the behavior clear to outside callers
In general everything changing small database objects can go into the repository. More complex operations have to be done in the service.
Another approach is moving objects into the domain classes, but be aware that you need to add @JsonIgnore where necessary:
@JsonIgnore
default boolean isLocked() {
if (this instanceof ProgrammingExerciseStudentParticipation) {
[...]
}
return false;
}