- Java 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build and Test / Java 21 - Complete Build (push) Successful in 45s
|
||
| .github/workflows | ||
| .mvn | ||
| annotations | ||
| purity-plugin | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE.txt | ||
| medium-article.md | ||
| mvnw | ||
| mvnw.cmd | ||
| NOTICE | ||
| pom.xml | ||
| README.md | ||
| RELEASING.md | ||
Purity Plugin
Maven Central | Specification | Changelog | Contributing | License
A Maven Error Prone plugin which gives you compiler support for pure functions in Java. This gives you side effect control, which in turn drives your application's architectural design when doing functional programming.
In Haskell, which is a functional language, pure functions are enforced through the compiler via the type system by making impure functions return an IO monad. Haskell regards all functions as pure by default, and pure functions are not allowed to call impure functions. This plugin is inspired by this approach.
This plugin enforces purity constraints between functions based on the @Impure and @Pure annotations.
It uses a configurable list of packages where all functions are considered pure by default (including sub-packages) unless explicitly marked otherwise with the @Impure annotation.
Since Error Prone runs inside javac as a compiler plugin, it analyzes the same typed AST/symbol data the compiler already builds. This gives great performance compared to traditional Maven plugins because no separate parser or type checker is needed. The Purity plugin will typically only add a few seconds of extra compilation time depending on the size of your application.
It's also recommended that Purity is used together with the Result framework for functional error handling. Together, they form a very strong programming model for functional programming in Java.
Requires Java 21 or higher.
Why Use Purity
As most developers know, once software reaches a certain level of complexity it very often suffers from a gradual decay of quality over time. This has a very real business impact, as it makes development speed slow down. Sometimes to the point where it grinds to a complete halt, and developers call for a rewrite. In software terminology this is called code entropy.
Entropy is fundamentally caused by teams lacking either the architectural competence or the engineering discipline needed to maintain application architecture manually over time. Too tight deadlines and dysfunctional organizations compound the problem further.
Undisciplined state mutation is one of the biggest sources of bugs in software development, and a big contributor to code entropy over time. The Purity plugin helps to address this problem by using the compiler to enforce side effect control. It forces the programmer to think about side effects in a much more systematic way. It forces the programmer to think about where business logic is (pure functions) and where application input/output is (impure functions), and thereby a big part of the architectural design. The Purity plugin also offloads the mental burden of manually maintaining this design.
The plugin is a strong side effect control mechanism. Instead of relying only on code review to spot hidden side effects, you can let the compiler enforce where side effects are allowed. Combined with e.g. ArchUnit to enforce modularization (and other rules), it gives you effective tooling to avoid long term entropy and thereby keep delivering business value.
When working with AI agents, then Purity in combination with Error Prone should be part of a strong safety stack. This will catch bugs early, give AI agents a fast feedback loop and keep them from generating bad code. ArchUnit, NullAway and SpotBugs should also be considered as part of a strong safety stack.
Maven Setup
In the Maven pom.xml, add the following to include Purity with Error Prone.
Change the config parameters, e.g. Purity:RootPackages, to match your application's package structure.
Note that Purity can also be used from Gradle through the Error Prone Gradle plugin.
<properties>
<purity.version>1.0.0</purity.version>
<errorprone.version>2.49.0</errorprone.version>
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>no.nhn.purity</groupId>
<artifactId>annotations</artifactId>
<version>${purity.version}</version>
<scope>provided</scope> <!-- The annotations are not present runtime, only compile time, so use scope provided. -->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-XDaddTypeAnnotationsToSymbol=true</arg>
<arg>--should-stop=ifError=FLOW</arg>
<arg>
-Xplugin:ErrorProne
-XepOpt:Purity:RootPackages=no.nhn
-XepOpt:Purity:StrictPurePackages=no.nhn.hit.criticalinformation.core,no.nhn.hit.criticalinformation.*.core
-XepOpt:Purity:ExcludedPaths=.*/src/test/java/.*,.*/generated-sources/.*
</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
<path>
<groupId>no.nhn.purity</groupId>
<artifactId>purity-plugin</artifactId>
<version>${purity.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Usage
The plugin takes in a comma-separated list of packages via config parameter Purity:RootPackages and regards all functions in the given packages and sub-packages as pure by default (no @Pure annotations
needed).
The programmer needs only annotate the impure functions with @Impure.
If a class is annotated with @Impure then all functions in the class will be regarded as impure unless any of them are annotated with @Pure.
In short, for the configured packages (including sub-packages), if a method is annotated with @Impure it's impure.
If a class is annotated with @Impure, all its methods are impure unless explicitly marked @Pure.
Methods not annotated with @Impure or @Pure are considered pure by default.
Getting Started
- First, set up the Error Prone plugin and Purity in
pom.xml. - Then set the required config parameter
Purity:RootPackageswhich typically will point at the root package of your application. This will make the plugin regard all functions as pure by default in this package and sub-packages. - Then mark all functions or classes which are not pure with
@Impure. Fix any purity errors in your code base which may follow from doing this. - Then look at the config parameters below and see if you need any of them.
- The parameter
Purity:StrictPurePackagesis recommended to avoid that developers accidentally introduce impure functions in packages meant to always be pure. - The parameter
Purity:ExcludedPathscan be used to disable the plugin when running the tests, or exclude a given file or package, as shown below. - To disable the plugin for a single function or class you can annotate it with
@SuppressWarnings("Purity"). - When adding new code later you must mark impure code with
@Impure.
Config Parameters
You can use a comma-separated list with max 100 elements with the parameters. You can also use * in the package names for all parameters, e.g. com.myapp.*.core. Sub-packages are always included.
Purity:RootPackages
The only required parameter. Typically points at the root package of your application. The plugin regards all functions as pure by default in this package and sub-packages.
You must manually mark all functions or classes which are not pure with @Impure and fix any purity errors in your code base which may follow from doing this.
-XepOpt:Purity:RootPackages=no.nhn
Purity:StrictPurePackages
If you've got some packages which shall never contain impure functions under any circumstance, typically domain/core-packages in the Functional Core, you can use this optional parameter
to enforce this. This will cause an error if someone adds an @Impure annotation in the package or sub-packages by mistake.
-XepOpt:Purity:StrictPurePackages=no.nhn.hit.criticalinformation.core,no.nhn.hit.criticalinformation.*.core
Purity:ExcludedPaths
If you need to disable the plugin when running the tests, or exclude a given file or package, then use this optional config parameter. It does regex-based path filtering. Multiple regex expressions must be separated by comma. Typically used when introducing Purity in legacy applications.
To exclude the tests:
-XepOpt:Purity:ExcludedPaths=.*/src/test/java/.*
To exclude the tests, the criticalinformation.core-package, the file ConditionExtraInfo.java and the /build/generated-path:
-XepOpt:Purity:ExcludedPaths=.*/src/test/java/.*,.*/criticalinformation/core/.*,.*/condition/core/ConditionExtraInfo.java.*,.*/build/generated/.*
Other
As a standard part of Error Prone you can adjust the severity level of errors for the plugin.
-Xep:Purity:WARN
Annotation Examples
The examples assume that the classes are inside the package, or sub-packages, set by the config parameter Purity:RootPackages.
public class MyClass {
public int pureMethod1() {
return 1 + 1;
}
public void pureMethod2() {
pureMethod1(); // This is allowed, all functions are pure by default and pure functions can call other pure functions.
}
}
import no.nhn.purity.annotations.*;
public class MyClass {
@Impure
public void impureMethod1() {
System.out.println("Side effect1");
}
public void pureMethod() {
impureMethod1(); // BUG: Compile error, pure functions cannot call impure functions.
}
}
import no.nhn.purity.annotations.*;
@Impure // Makes all functions impure, but can be overridden using @Pure.
public class MyClass {
public void impureMethod1() {
System.out.println("Side effect1");
}
@Pure
public void pureMethod() {
impureMethod1(); // BUG: Compile error, pure functions cannot call impure functions.
}
}
import no.nhn.purity.annotations.*;
public class MyClass {
public void pureMethod() {
System.out.println("Side effect"); // BUG: Compile error, pure functions cannot call impure functions in the JDK.
}
}
Architectural Compiler Support
As mentioned above, the plugin gives you side effect control via the compiler, which in turn drives your application's architectural design. The architectural design this naturally results in is FC/IS (Functional Core/Imperative Shell) from functional programming.
The pure functions are in the Functional Core (typically domain/core-packages) and the impure functions are in the Imperative Shell (typically the infrastructure-packages).
There's only one, deceptively simple, rule in this architectural design: Pure functions cannot call impure functions.
This means that the programmer must separate pure code (business logic) from impure code (application input/output).
A pure function is a function which guarantees the same output given the same input. This means you e.g. cannot call System.out, date classes or random generators from pure functions, but have to send these in as parameters instead.
The plugin ensures the source code follows this rule with help from the compiler. This means that dependencies between function-calls always point inwards towards the Functional Core by verifying that the impure functions can call the pure ones, but the pure functions cannot call the impure ones. If a violation occurs an Error Prone diagnostic error will be displayed when the application compiles. This is called the The Dependency Rule and is originally coined by Robert Martin in Clean Architecture.
Like any architectural design, the FC/IS design is also fragile when maintained manually. It's easy to break function purity in non-functional languages. Especially when working in teams where members have different kinds of backgrounds and experience levels, it's all too easy for the architectural design to fall apart over time. This will cause the application to get harder and harder to maintain as it ages, and eventually it will go into complete entropy.
This plugin addresses that problem by using the compiler to make the architectural design much harder to break. It forces the programmer's hand much the same way Haskell does. The plugin therefore represents a repeatable engineering-based approach to software design with support from the compiler to enforce it. This stands in contrast to the way most software is written where design and architecture is either based on discretion which is not repeatable, or there's no design at all. This will in turn, in most cases, lead to maintenance problems as the application grows and ages with the typical rewrite after a few years. This is especially prevalent in large organizations with many teams and developers with different kinds of backgrounds and experience levels.
The architectural design of FC/IS is also very simple, and avoids much of the unnecessary abstraction that often comes with object-oriented architectural designs.
Imperative Shell
HTTP • DB • Kafka • Logging • Files • Controllers
│
│ Dependency rule (pure functions cannot call impure functions)
▼
Functional Core
Pure functions with business logic
About Architecture
Architecture is about handling coupling and cohesion in the application. Low coupling and high cohesion is the hallmark of good software design.
By cohesion we mean functional cohesion, i.e. all code which functionally belongs together should be together (seen from the perspective of a client/user). By coupling we mean the interconnected function calls and objects.
Low coupling and high cohesion will in turn ensure that the application is easy to change and easy to understand as it grows and ages. The code must be maintainable over time, and that is the prime directive. The beauty of good code isn’t in its complexity but its readability. Like a well-written book it should be easy to understand, follow and modify.
The three simple rules below represent the essence of architectural thinking within enterprise software the last few decades, and is what this plugin helps to support.
- Group your application on functional subdomains (bounded contexts or feature slices). This keeps the application modularized. Use e.g. ArchUnit to enforce modularization via tests.
- Put business logic in pure functions and input/output (I/O) in impure functions.
- Pure functions cannot call impure functions (dependency rule).
These rules are simple yet effective at taming both coupling and cohesion. They avoid much of the unnecessary abstraction and over-engineering that often comes with other architectural designs. They represent a repeatable engineering-based approach to software architecture where much discretion is removed from the design. This plugin gives support from the compiler to enforce point 2 and 3. Point 1 will always be up to the discretion of the programmer, but with help from e.g. ArchUnit to enforce the design.
Limitations
Please note that even though the plugin does a good job in automatically detecting impure code, it isn't perfect. It cannot infer every side effect in all code paths or libraries. For that to happen the JDK itself needs to have compiler support for pure functions, which it does not.
Because of this, you will sometimes need to manually mark code as @Impure even if the plugin does not detect it as impure first.
It should be part of architectural discipline that if a method is impure by design, mark it explicitly.
In short, use the plugin as strong compiler support, not as a complete substitute for engineering judgment.
The plugin runs through Error Prone inside javac and is therefore Java-focused. Kotlin method bodies are not analyzed in the same way, so mixed Java/Kotlin code bases may still need extra discipline at language boundaries.
The plugin verifies the following constraints in pure methods:
- Pure methods cannot call functions annotated with
@Impure. - Pure methods cannot create lambdas or method references which call functions annotated with
@Impure. - Pure methods cannot call known impure methods in the JDK and a few selected third-party APIs.
- Pure methods cannot write to fields from non-constructor methods (including static fields).
- Pure classes cannot declare static mutable state, including
static final Date/Calendarfields. Static final collections/maps must use known immutable factories, wrappers, types or same-class static helper methods returning those, e.g.List.of,Stream.toList,Collectors.toUnmodifiable*,Map.copyOf,Collections.unmodifiable*or GuavaImmutable*. - Pure methods cannot use Java synchronization, either as
synchronizedmethods orsynchronizedblocks. - Pure methods cannot call known synchronization/coordination APIs such as locks, conditions, semaphores, latches, barriers, phasers or exchangers.
- Pure methods cannot call reflection mutation APIs such as
Field.set*,setAccessibleortrySetAccessible. - Pure methods cannot mutate
Collection/Mapfields onthisfrom non-constructor methods. - Pure methods cannot call mutating setter methods on
this. - Pure methods cannot call mutating setter methods on input parameters (or aliases of input parameters).
- Pure methods cannot mutate input collections/maps (including mutating
Collectionsutility methods). - Pure methods cannot mutate input arrays.
- Pure methods cannot call mutating methods on mutable wrapper input parameters (e.g.
Atomic*,StringBuilder,ByteBuffer).
License
Copyright 2026 Norsk Helsenett SF.
The framework is licensed under the Apache License, Version 2.0. See LICENSE and NOTICE for details.