Server architecture gates
Artemis enforces its server conventions with a large ArchUnit suite under src/test/java, most of
it module-scoped subclasses of a handful of abstract rule bases. They are not style preferences. Each one exists because the pattern it forbids
produced a production bug. The failure messages are often terse, so this skill maps a change to the
rules it is subject to and to the reason behind each.
Run them locally
The whole architecture suite, which is what the Server Code Style job runs:
./gradlew test -DincludeTags='ArchitectureTest' -x webapp
This is much faster than the full server test suite. Run it before pushing any change to
src/main/java. A violation fails both Server Code Style and Server Tests, so it is worth catching
locally.
A single class while iterating:
./gradlew test --tests ArchitectureTest -x webapp
Which rules apply to what you changed
| You changed | Read |
|---|---|
| A service or REST resource | Transactions, persistence access, module boundaries |
| A repository | Transactions, raw JDBC |
| A DTO record | DTO conventions |
| Anything holding state across requests | Caching, distributed data |
| An entity or an association | Caching, entity conventions |
| Anything at all in a large file | Counted gates |
| Anything that lowercases or uppercases | Case conversion |
| Anything that serializes JSON | Jackson version |
The detail for each, with the reason and the failing rule name, is in reference/gates.md. Read
it rather than guessing; several of these rules forbid something that looks completely reasonable.
Jackson 2 must not appear in production code. Artemis serializes with Jackson 3, whose packages are
tools.jackson. Jackson 2 stays on the runtime classpath for third-party libraries that carry their own
mapper, so a com.fasterxml.jackson.databind, .core, .dataformat, .datatype, .module, .jr or
.jaxrs import still compiles — testNoJackson2InProductionCode in ArchitectureTest is what rejects it.
The one exception is com.fasterxml.jackson.annotation: jackson-annotations never moved to the
tools.jackson group, so @JsonInclude, @JsonProperty and @JsonTypeInfo stay where they are and must
not be "fixed". Mappers are immutable in Jackson 3 — derive one with JsonMapper.builder() or
rebuild(), never configure() or registerModule() on a built instance — and its exceptions are
unchecked, so a catch (IOException) no longer catches a parse failure.
The rules most often broken
No transaction boundaries in services or controllers. @Transactional,
TransactionTemplate, and PlatformTransactionManager belong in repositories, typically on
modifying queries, and TransactionSynchronizationManager is banned outright. Enforced globally by
testTransactionBoundariesOnlyInRepositories, testNoProgrammaticTransactionManagement and
testNoTransactionSynchronization in
src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java. The replacements
are a check in the WHERE clause of a @Modifying query, or explicit compensation in a catch
block.
No direct persistence access. No injected EntityManager or EntityManagerFactory, and no
JdbcClient, JdbcTemplate, or DataSource. Write the statement as a @Query on a repository,
with nativeQuery = true where there is no entity to name. Enforced by
shouldNotUseEntityManagerDirectly and shouldNotUseRawJdbcDirectly in
src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java.
Three classes sit on that rule's exception list, carrying a TODO to refactor them away. One of them
is TitleCacheEvictionService, which holds an EntityManagerFactory purely to reach the Hibernate
EventListenerRegistry and register itself as a listener. So when the caching section below calls
it the canonical eviction pattern, copy its eviction logic, not its constructor: a new class doing
the same thing fails the rule, because the list is grandfathering rather than permission. Raw JDBC
has no per-class exceptions at all; only core.config may hold a DataSource.
Never touch Hazelcast or Redis directly. All cross-node state goes through
DistributedDataProvider in
src/main/java/de/tum/cit/aet/artemis/core/service/distributed/. Enforced by
src/test/java/de/tum/cit/aet/artemis/shared/architecture/DistributedDataProviderArchitectureTest.java.
The provider is configurable, so direct usage does not fail loudly, it silently loses the state.
No Hibernate second-level cache. No @Cache on entities or associations. Enforced by
testNoHibernateSecondLevelCacheAnnotation in ArchitectureTest.java. For DTO and projection
caching use Spring @Cacheable, always paired with explicit eviction.
Reach optional modules through their API. Use Optional<*Api>, never another module's
repository directly.
Never fold case without a locale. String.toLowerCase() and String.toUpperCase() use the JVM
default locale, so the same input gives a different answer depending on where the server runs. Pass
Locale.ROOT for machine-facing values and Locale.ENGLISH only where the surrounding code already
does for that kind of value. Enforced by testNoLocaleLessCaseConversion in ArchitectureTest.java,
over production and test classes both.
Before adding a cache
The default answer is not to. The bar is a measured performance gain that justifies the
eviction-correctness work, because there is no service-level transaction boundary to coordinate
eviction within a request. See documentation/docs/developer/guidelines/caching.mdx for the full
rationale, and reference/gates.md for the pattern if you do proceed.
Adding a capability to the distributed data layer
If DistributedDataProvider lacks what you need, add it there, implement it for all three providers
(Hazelcast, Redis, Local), and add a case to AbstractDistributedDataTest. That suite is what keeps
the providers in agreement. Request entry lifetimes at the call site with
getExpiringMap(name, ttl); getMap(name) rejects a per-entry TTL deliberately, because a provider
map configuration only applies to that one provider. Full guidance:
documentation/docs/developer/guidelines/distributed-data.mdx.