- name
- server-arch-gates
- description
- Check Artemis server code against the architectural rules the build enforces, before pushing. Use when writing or changing Java under src/main/java, adding a service, repository, REST resource, DTO, cache, or cross-node state, or when an ArchUnit test fails and the message does not make the rule obvious. Gives the rule, the reason, and the exact local command that proves it.
# 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:
```bash
./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:
```bash
./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`.
GitHubで見る