Writing Scala in Rudder
Rudder is a long-lived codebase (Scala 2.8 in 2009 → Scala 3 today). We migrate
step by step toward a more functional style with ZIO as the effect handler.
New and changed code follows the conventions below; we do not rewrite untouched
legacy code just to modernize it, but we leave every file we touch a little better.
Scope — applies to all the Rudder Scala repos. These conventions hold for the Scala
code in rudder, rudder-plugins, and rudder-plugins-private alike (the plugin
repos build on rudder-core and use the same stack: IOResult/RudderError, zio-json,
chimney, quicklens, enumeratum, the plugin API framework, etc.). This skill file lives
in the rudder repo, and its links to ADRs and example code are relative to that repo —
but when you write Scala in either plugins repo, follow this skill too (look up the
referenced rudder code/ADR in the rudder checkout). Plugin-specific points: the
dynamic plugin-status API framework (103) and license
parsing live in the plugin repos.
Golden rules (always apply)
- Functional first. Immutable data, no shared mutable state, effects reified as
values. Any mutation or I/O must be wrapped in
IOResult (see 300).
- Less code is better code. Minimize boilerplate and lines. Prefer the simple,
short solution. Don't implement a pattern "fully" if a smaller version is clearer.
- No type-level acrobatics. Scala is powerful — lean on that power, but get it
from well-chosen libraries (zio, chimney, quicklens, zio-json), not from
hand-rolled implicit/type-level machinery.
- Dumb data, smart companions. Business objects are plain
case classes with as
few methods as possible. Serialization, conversion and mapping live in companion
objects or extension methods (see 001, 400).
- Program to traits. Every service/repository has a trait defining its API, even
with a single implementation. DI is constructor-only, wired in
RudderConfig
(see 102).
- Parse, don't validate. User input is parsed into typed domain objects at the
boundary; persistence goes through a repository (see
200, 201).
- Security in depth is a design constraint, not an afterthought (see
600).
- Be strict about dependencies. Apache2/BSD-compatible licenses only; prefer
removing deps over adding them (see
700).
How to use this skill
Before doing substantial work in a given area, read the matching topic file. Files
are named NNN-topic.md; the first digit is the subject area (table below), the
other two digits identify the topic within it.
| 1st digit |
Subject area |
| 0 |
Generalities — coding philosophy & Scala 3 idioms |
| 1 |
Architecture — DDD, hexagonal, traits & dependency injection |
| 2 |
Persistence — repositories, parse-don't-validate |
| 3 |
Effects & errors — ZIO, IOResult, bridging legacy code |
| 4 |
Data & serialization — case classes, zio-json, chimney, quicklens |
| 5 |
Datetime — java.time |
| 6 |
Security — defense in depth, web/output safety, authn/authz |
| 7 |
Dependencies & ecosystem |
| 8 |
Build, tooling & formatting |
| 9 |
Testing |
Topic index
000-coding-philosophy.md — FP, immutability, minimal LOC, no type acrobatics
001-scala3-idioms.md — companions, extensions, derives, given, opaque types, minimizing imports
100-package-layout.md — bounded-context-first packages, not technical-layer; migrating off legacy domain/repository/services
101-architecture-ddd-hexagonal.md — bounded contexts, hexagonal, "pragmatic DDD"
102-traits-and-dependency-injection.md — program-to-trait, constructor DI, RudderConfig
103-rest-api-and-endpoints.md — declarative EndpointSchema, versioning, per-endpoint authz, lift handlers
104-bootstrap-and-migrations.md — BootstrapChecks early/end phases, self-managed async DB/schema migrations, Boot.scala
200-persistence-repositories.md — repository as single point of change, optional persistence layer
201-parse-dont-validate.md — typed parsing at I/O boundaries
300-effects-zio-ioresult.md — IOResult / PureResult, when & how to use ZIO
301-error-model.md — RudderError, Chained, Accumulated, domain errors
302-bridging-toio-runnow.md — .toIO, .toBox, minimizing .runNow
303-logging.md — NamedZioLogger pure loggers, migrating off lift extends Logger
400-domain-case-classes.md — dumb case classes, value/opaque wrappers, ADTs
401-json-zio-json.md — derives JsonCodec, manual codecs, discriminators
402-chimney-transformers.md — mapping between representations
403-quicklens-updates.md — .modify(...).setTo(...) instead of .copy
404-serialization-contracts.md — user-facing serialization is a contract; DTOs + chimney, stable tokens, test-enforced
500-datetime-java-time.md — java.time, migrating off joda
600-security-in-depth.md — SecurityError, tenants, path traversal, defense in depth
601-web-and-output-security.md — XmlSafe (XXE), Lift JsRaw/XSS, CSRF, CSP, sessions
602-authentication-and-authorization.md — Spring auth vs Rudder authz, BouncyCastle, tokens, RBAC/ACL AuthorizationType
700-dependencies-ecosystem.md — license policy, minimizing deps, ZIO scope
800-build-and-formatting.md — maven/spotless/scalafmt, -Werror, license headers
900-testing.md — specs2 + JUnitRunner, zio-test for new code, trait-based test doubles
The effect type is com.normation.errors.IOResult[A] = ZIO[Any, RudderError, A].
Sources of truth
The error-management philosophy behind RudderError/IOResult (nominal cases vs
errors vs defects, WYSIWYG contracts, errors-as-signal) comes from the talk
"Systematic error management in application" (DevoxxFR 2021, F. Armand).
See 000, 301.
This skill summarizes and operationalizes decisions; the authoritative records are
the ADRs in rudder/adr/webapp/ (and some in
rudder/adr/system/). When a topic touches an ADR, the topic
file cites it — follow the link and read the ADR for the full context and rationale,
and prefer the ADR if it ever conflicts with the summary here. If you make a decision
that isn't captured yet, add an ADR (see adr/template.md)
and then update the matching topic file. ADRs already reflected here include:
18879 zio-json is the only JSON library → 401
28515 use java.time (prefer Instant) and 27084 explicit-timezone/RFC-3339
dates → 500
28452 safe QueryContext in Lift snippets (SecureDispatchSnippet) → 600
28612 central plugin-status checks for menus/APIs → 103
1---2name: rudder-scala3description: Conventions and idioms for writing Scala in the Rudder codebase (webapp/sources). Use whenever reading, writing, reviewing, or refactoring Scala in the rudder repo. Covers functional style, ZIO effects and the RudderError model (IOResult), DDD / hexagonal architecture, repositories, parse-don't-validate, zio-json / chimney / quicklens data handling, java.time, security-in-depth, and dependency policy.4---56# Writing Scala in Rudder78Rudder is a long-lived codebase (Scala 2.8 in 2009 → Scala 3 today). We migrate9**step by step** toward a more functional style with **ZIO** as the effect handler.10New and changed code follows the conventions below; we do not rewrite untouched11legacy code just to modernize it, but we leave every file we touch a little better.1213**Scope — applies to all the Rudder Scala repos.** These conventions hold for the Scala14code in `rudder`, **`rudder-plugins`, and `rudder-plugins-private`** alike (the plugin15repos build on rudder-core and use the same stack: `IOResult`/`RudderError`, zio-json,16chimney, quicklens, enumeratum, the plugin API framework, etc.). This skill file lives17in the `rudder` repo, and its links to ADRs and example code are relative to that repo —18but when you write Scala in either plugins repo, **follow this skill too** (look up the19referenced rudder code/ADR in the `rudder` checkout). Plugin-specific points: the20dynamic plugin-status API framework ([`103`](103-rest-api-and-endpoints.md)) and license21parsing live in the plugin repos.2223## Golden rules (always apply)24251. **Functional first.** Immutable data, no shared mutable state, effects reified as26 values. Any mutation or I/O *must* be wrapped in `IOResult` (see `300`).272. **Less code is better code.** Minimize boilerplate and lines. Prefer the simple,28 short solution. Don't implement a pattern "fully" if a smaller version is clearer.293. **No type-level acrobatics.** Scala is powerful — lean on that power, but get it30 from well-chosen *libraries* (zio, chimney, quicklens, zio-json), not from31 hand-rolled implicit/type-level machinery.324. **Dumb data, smart companions.** Business objects are plain `case class`es with as33 few methods as possible. Serialization, conversion and mapping live in companion34 objects or `extension` methods (see `001`, `400`).355. **Program to traits.** Every service/repository has a trait defining its API, even36 with a single implementation. DI is constructor-only, wired in `RudderConfig`37 (see `102`).386. **Parse, don't validate.** User input is parsed into typed domain objects at the39 boundary; persistence goes through a repository (see `200`, `201`).407. **Security in depth** is a design constraint, not an afterthought (see `600`).418. **Be strict about dependencies.** Apache2/BSD-compatible licenses only; prefer42 removing deps over adding them (see `700`).4344## How to use this skill4546Before doing substantial work in a given area, read the matching topic file. Files47are named `NNN-topic.md`; the **first digit** is the subject area (table below), the48other two digits identify the topic within it.4950| 1st digit | Subject area |51|-----------|--------------|52| 0 | Generalities — coding philosophy & Scala 3 idioms |53| 1 | Architecture — DDD, hexagonal, traits & dependency injection |54| 2 | Persistence — repositories, parse-don't-validate |55| 3 | Effects & errors — ZIO, `IOResult`, bridging legacy code |56| 4 | Data & serialization — case classes, zio-json, chimney, quicklens |57| 5 | Datetime — `java.time` |58| 6 | Security — defense in depth, web/output safety, authn/authz |59| 7 | Dependencies & ecosystem |60| 8 | Build, tooling & formatting |61| 9 | Testing |6263### Topic index6465- [`000-coding-philosophy.md`](000-coding-philosophy.md) — FP, immutability, minimal LOC, no type acrobatics66- [`001-scala3-idioms.md`](001-scala3-idioms.md) — companions, extensions, `derives`, `given`, opaque types, minimizing imports67- [`100-package-layout.md`](100-package-layout.md) — bounded-context-first packages, not technical-layer; migrating off legacy `domain`/`repository`/`services`68- [`101-architecture-ddd-hexagonal.md`](101-architecture-ddd-hexagonal.md) — bounded contexts, hexagonal, "pragmatic DDD"69- [`102-traits-and-dependency-injection.md`](102-traits-and-dependency-injection.md) — program-to-trait, constructor DI, `RudderConfig`70- [`103-rest-api-and-endpoints.md`](103-rest-api-and-endpoints.md) — declarative `EndpointSchema`, versioning, per-endpoint `authz`, lift handlers71- [`104-bootstrap-and-migrations.md`](104-bootstrap-and-migrations.md) — `BootstrapChecks` early/end phases, self-managed async DB/schema migrations, `Boot.scala`72- [`200-persistence-repositories.md`](200-persistence-repositories.md) — repository as single point of change, optional persistence layer73- [`201-parse-dont-validate.md`](201-parse-dont-validate.md) — typed parsing at I/O boundaries74- [`300-effects-zio-ioresult.md`](300-effects-zio-ioresult.md) — `IOResult` / `PureResult`, when & how to use ZIO75- [`301-error-model.md`](301-error-model.md) — `RudderError`, `Chained`, `Accumulated`, domain errors76- [`302-bridging-toio-runnow.md`](302-bridging-toio-runnow.md) — `.toIO`, `.toBox`, minimizing `.runNow`77- [`303-logging.md`](303-logging.md) — `NamedZioLogger` pure loggers, migrating off lift `extends Logger`78- [`400-domain-case-classes.md`](400-domain-case-classes.md) — dumb case classes, value/opaque wrappers, ADTs79- [`401-json-zio-json.md`](401-json-zio-json.md) — `derives JsonCodec`, manual codecs, discriminators80- [`402-chimney-transformers.md`](402-chimney-transformers.md) — mapping between representations81- [`403-quicklens-updates.md`](403-quicklens-updates.md) — `.modify(...).setTo(...)` instead of `.copy`82- [`404-serialization-contracts.md`](404-serialization-contracts.md) — user-facing serialization is a contract; DTOs + chimney, stable tokens, test-enforced83- [`500-datetime-java-time.md`](500-datetime-java-time.md) — `java.time`, migrating off joda84- [`600-security-in-depth.md`](600-security-in-depth.md) — `SecurityError`, tenants, path traversal, defense in depth85- [`601-web-and-output-security.md`](601-web-and-output-security.md) — `XmlSafe` (XXE), Lift `JsRaw`/XSS, CSRF, CSP, sessions86- [`602-authentication-and-authorization.md`](602-authentication-and-authorization.md) — Spring auth vs Rudder authz, BouncyCastle, tokens, RBAC/ACL `AuthorizationType`87- [`700-dependencies-ecosystem.md`](700-dependencies-ecosystem.md) — license policy, minimizing deps, ZIO scope88- [`800-build-and-formatting.md`](800-build-and-formatting.md) — maven/spotless/scalafmt, `-Werror`, license headers89- [`900-testing.md`](900-testing.md) — specs2 + JUnitRunner, zio-test for new code, trait-based test doubles9091> The effect type is `com.normation.errors.IOResult[A] = ZIO[Any, RudderError, A]`.9293## Sources of truth9495The **error-management philosophy** behind `RudderError`/`IOResult` (nominal cases vs96errors vs defects, WYSIWYG contracts, errors-as-signal) comes from the talk97**"Systematic error management in application"** (DevoxxFR 2021, F. Armand).98See [`000`](000-coding-philosophy.md), [`301`](301-error-model.md).99100This skill summarizes and operationalizes decisions; the **authoritative records are101the ADRs** in [`rudder/adr/webapp/`](../../../adr/webapp/) (and some in102[`rudder/adr/system/`](../../../adr/system/)). When a topic touches an ADR, the topic103file cites it — **follow the link and read the ADR** for the full context and rationale,104and prefer the ADR if it ever conflicts with the summary here. If you make a decision105that isn't captured yet, add an ADR (see [`adr/template.md`](../../../adr/template.md))106and then update the matching topic file. ADRs already reflected here include:107108- `18879` zio-json is the only JSON library → [`401`](401-json-zio-json.md)109- `28515` use `java.time` (prefer `Instant`) and `27084` explicit-timezone/RFC-3339110 dates → [`500`](500-datetime-java-time.md)111- `28452` safe `QueryContext` in Lift snippets (`SecureDispatchSnippet`) → [`600`](600-security-in-depth.md)112- `28612` central plugin-status checks for menus/APIs → [`103`](103-rest-api-and-endpoints.md)