Server architecture gates

SkillDev tools

Apply Artemis architecture rules when changing server Java or diagnosing an ArchUnit failure.

Available today. Use it from your connected AI after setup.

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 changedRead
A service or REST resourceTransactions, persistence access, module boundaries
A repositoryTransactions, raw JDBC
A DTO recordDTO conventions
Anything holding state across requestsCaching, distributed data
An entity or an associationCaching, entity conventions, column mapping
Anything at all in a large fileCounted gates
Anything that lowercases or uppercasesCase conversion
Anything that serializes JSONJackson version
A websocket topic or message handlerWebsocket 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