Server architecture gates
SkillDev toolsApply Artemis architecture rules when changing server Java or diagnosing an ArchUnit failure.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Server architecture gates skill
What this skill tells your AI
The instructions your AI receives, as published by ls1intum/artemis in skills/server-arch-gates/SKILL.md and read by ahel’s review.
Artemis enforces server conventions with ArchUnit tests under src/test/java. This skill maps
server changes to those checks and explains rules whose failure messages lack context.
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, column mapping |
| Anything at all in a large file | Counted gates |
| Anything that lowercases or uppercases | Case conversion |
| Anything that serializes JSON | Jackson version |
| A websocket topic or message handler | Websocket topics |
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 @Lob. A CLOB on PostgreSQL is a large object, so the value lands in pg_largeobject and the
column keeps only its id - while the long text columns here are Liquibase longtext or clob, both
text on PostgreSQL, holding the text itself. A String or a converted attribute needs no
annotation at all; for a structured value use @JdbcTypeCode(SqlTypes.JSON) over a json column.
Enforced by testNoLobAnnotation in ArchitectureTest.java.
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.
Every websocket destination is a declared topic. Declare a WebsocketTopic with its
WebsocketTopicAccess rule, or a WebsocketUserTopic for data of one user, as a public static final
constant of the module's web/<Module>WebsocketTopics class, and send with
websocketMessagingService.sendMessage(TOPIC.at(id), dto). A subscription to an undeclared destination
is rejected, so a topic that is sent but not declared silently reaches nobody. Clients send only to
/app/... destinations handled by @MessageMapping methods, which check the sender themselves.
Enforced by WebsocketTopicArchitectureTest in
src/test/java/de/tum/cit/aet/artemis/shared/architecture/.
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.
Signals
- GitHub stars
- 813
- Forks
- 396
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
server-arch-gates- Source
- github.com/ls1intum/artemis