Recent Posts

All Posts →

Call by Value vs. Call by Reference

Defines call by value and call by reference from the Dragon Book and Strachey, then compares how Java, Kotlin, JavaScript, C, C++, C#, Python, Go, Rust, and Ada pass arguments to functions.

You often hear that "in Java method calls, primitive types are passed by call by value and objects are passed by call by reference." Java, however, has only call by value. When you pass an object to a method, the value that gets copied is simply a reference that points to the object. This article lays out the definitions of the two terms and compares how Java, Kotlin, JavaScript, C, C++, C#, Python, Go, Rust, and Ada pass arguments.

Call by Value and Call by Reference

Call by value initializes a separate variable, the parameter, with the value passed as the argument. The value is usually copied, though some languages such as Rust move ownership instead. Assigning a different value to the parameter inside the function does not affect the caller’s variable.

Call by reference passes the variable itself. The parameter becomes an alias of the caller’s variable, so assigning a new value to the parameter inside the function also changes the caller’s variable.

These definitions follow the standard treatment in programming language theory.

Section 1.6.6 of Compilers: Principles, Techniques, and Tools, 2nd edition, describes the two mechanisms as follows.

In call-by-value, the actual parameter is evaluated (if it is an expression) or copied (if it is a variable). The value is placed in the location belonging to the corresponding formal parameter of the called procedure. This method is used in C and Java, and is a common option in C++, as well as in most other languages. …​

In call-by-reference, the address of the actual parameter is passed to the callee as the value of the corresponding formal parameter. Uses of the formal parameter in the code of the callee are implemented by following this pointer to the location indicated by the caller. Changes to the formal parameter thus appear as changes to the actual parameter.

— Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman
Compilers: Principles, Techniques, and Tools, 2nd edition, Section 1.6.6

The "actual parameter" in the quote corresponds to what this article calls an argument, and the "formal parameter" corresponds to what this article calls a parameter. Together with its 1977 predecessor Principles of Compiler Design, this book has served as the standard textbook for university compiler courses. Its cover illustration earned it the nickname "the Dragon Book."

Cover of Compilers: Principles, Techniques, and Tools, 2nd edition
Figure 1. Cover of Compilers: Principles, Techniques, and Tools, 2nd edition (image source: Open Library)

Christopher Strachey explained the distinction in terms of L-values and R-values in Section 3.4.2 of Fundamental Concepts in Programming Languages, his 1967 lecture notes. He called passing the R-value of an expression to a parameter call by value, and passing its L-value call by reference.

When the function is used (or called or applied) we write f[ε] where ε can be an expression. If we are using a referentially transparent language all we require to know about the expression ε in order to evaluate f[ε] is its value. There are, however, two sorts of value, so we have to decide whether to supply the R-value or the L-value of ε to the function f. Either is possible, so that it becomes a part of the definition of the function to specify for each of its bound variables (also called its formal parameters) whether it requires an R-value or an L-value. These alternatives will also be known as calling a parameter by value (R-value) or reference (L-value).

— Christopher Strachey
Fundamental Concepts in Programming Languages, Section 3.4.2

An L-value is the storage location denoted by an expression on the left side of an assignment, and an R-value is the value stored in that location, denoted by an expression on the right side. These lecture notes are a classic of programming language theory and are still cited as the origin of terms such as L-value, R-value, and first-class object.

A practical test for telling the two mechanisms apart fits in one sentence: When a function assigns a new value to its parameter, does the caller’s variable change? If it does, the mechanism is call by reference; if it does not, it is call by value. The test holds under the assumptions of this article: only these two mechanisms are compared, the argument is a reassignable variable, and the parameter can be assigned to. Once other evaluation strategies such as call by copy-restore and call by name come into play, this question alone cannot classify every argument-passing mechanism.

The assignment in this test means putting a different value into the parameter variable itself, as in targetTv = new Tv(). If the parameter points to an object, this makes it point to a different object. Changing the internal state of the object the parameter points to, as in targetTv.setChannel("Rock"), is a separate matter. The misconception discussed later comes from confusing the two.

The Terms "Pass by" and "Call by"

Call by value and call by reference are also called pass by value and pass by reference. The call by ~ form dates back to the ALGOL 60 report published in 1960. Section 4.7.3 of the revised report of 1963 specifies how parameters are passed, with 4.7.3.1 titled "Value assignment (call by value)" and 4.7.3.2 titled "Name replacement (call by name)." The pass by ~ form is also widely used in practice and in textbooks. This article uses call by ~ throughout.

Call by Sharing

The argument-passing behavior of Java, JavaScript, and Python, which shares an object rather than the caller’s variable itself, is sometimes called call by sharing. The term comes from the design document of CLU, a language created in the mid-1970s by a team led by Barbara Liskov. The object-argument behavior of Java, Kotlin, JavaScript, and Python and the passing of class instances in C#, all covered later, fall into this category. The term "call by object reference" used in the Python tutorial means the same thing. The same structure appears when you pass a map or a pointer in Go, or a pointer in C.

"Call by value" can mislead people into thinking the whole object is copied, and "call by reference" can mislead them into thinking that reassignment is reflected in the caller’s variable. Call by sharing is a good term for avoiding both misconceptions. Some sources use it, such as the MDN JavaScript documentation quoted later, but it has not become a standard term in textbooks or language specifications. This article therefore continues with only the two categories of call by value and call by reference.

Java

Result of the swap Method

The output of the following code shows how Java passes arguments.

Testing a swap method
public class ReferenceTest {
    public static void main(String[] args) {
        String x1 = "Hello";
        String y1 = "World";
        swap(x1, y1); // (1)
        System.out.println(x1); // Hello

        int x2 = 1;
        int y2 = 2;
        swap(x2, y2); // (2)
        System.out.println(x2); // 1
    }

    static void swap(Object x, Object y) {
        Object temp = x;
        x = y; // (3)
        y = temp;
    }

    static void swap(int x, int y) {
        int temp = x;
        x = y;
        y = temp;
    }
}
  1. Copies of the reference values held by x1 and y1 are assigned to the parameters x and y.

  2. The int values are copied into the parameters of the swap(int, int) overload. Without this overload, references to autoboxed Integer objects would be passed to the Object parameters, with the same result.

  3. Assigning each other’s values to the parameters x and y inside the method does not change the caller’s x1, y1, x2, or y2.

By the test defined above, Java behaves as call by value for both primitive types and objects.

Why Reassigning a Parameter Does Not Affect the Caller’s Variable

Let’s look more closely at passing an object as a method parameter.

Reassigning a parameter inside a method
class Tv {
    private String channel;

    void setChannel(String channel) { this.channel = channel; }
    String getChannel() { return channel; }
}

public class ReassignTest {
    public static void main(String[] args) {
        var myTv = new Tv();
        myTv.setChannel("Classic");

        reassign(myTv); // (1)
        System.out.println(myTv.getChannel()); // Classic
    }

    static void reassign(Tv targetTv) {
        targetTv = new Tv(); // (2)
        targetTv.setChannel("Rock");
    }
}
  1. The reference value held by myTv in the main method is copied and assigned to the parameter targetTv of the reassign method. The two variables point to the same object, but they are different variables.

  2. A new object is assigned to the parameter targetTv. Only the parameter targetTv now points to the new object, and myTv in the main method still points to the original object. That is why the output is Classic.

After the reference value is copied

Mutating Object State: The Source of the Misconception

If you change the internal state of the object the parameter points to, instead of reassigning the parameter, the changed state is visible through the caller’s variable.

Changing object state inside a method without reassigning the parameter
public class ChangeStateTest {
    public static void main(String[] args) {
        var myTv = new Tv();
        myTv.setChannel("Classic");

        changeState(myTv); // (1)
        System.out.println(myTv.getChannel()); // (3)
    }

    static void changeState(Tv targetTv) {
        targetTv.setChannel("Rock"); // (2)
    }
}
  1. As in the previous example, the reference value held by myTv is copied and assigned to the parameter targetTv of the changeState method. The two variables point to the same object.

  2. Without reassigning the parameter targetTv, the method changes the channel of the object the parameter points to Rock. This is the very object that myTv in the main method points to.

  3. Reading the channel through myTv in the main method prints Rock. The variable did not change, but the state of the object it points to did.

Both variables point to the same object

Because the changed state is visible through the caller’s variable, the claim that "objects are passed by call by reference" sounds plausible. As explained above, however, the practical test for the two mechanisms is whether a value assigned to the parameter is reflected in the caller’s variable. The changed state is visible through the caller’s variable only because both variables point to the same object, and what is passed is still a copy of the reference value. In other words, it is call by value.

Head First Java explains object references with the analogy of a TV remote control. Passing an object does not copy the TV (the object); it copies the remote control (the reference value) that operates the TV. If you change the channel with the copied remote (changing object state), the person holding the original remote sees the new channel too. If you pair the copied remote with a new TV (reassignment, targetTv = new Tv()), that remote can no longer operate the original TV, and the original remote is still paired with the original TV.

Java References and Terminology Confusion

James Gosling, the creator of Java, addressed this misconception directly in The Java Programming Language, which he coauthored.

Some people will say incorrectly that objects are passed "by reference." …​ The Java programming language does not pass objects by reference; it passes object references by value.

— Ken Arnold, James Gosling, David Holmes
The Java Programming Language, 4th edition
Cover of The Java Programming Language, 4th edition
Figure 2. Cover of The Java Programming Language, 4th edition (image source: Open Library)

Dev.java, the current official Java learning site, says the same thing.

Reference data type parameters, such as objects, are also passed into methods by value. …​ when the method returns, the passed-in reference still references the same object as before.

— Dev.java
Calling Methods and Constructors

The Dragon Book quoted earlier also states in the same section that Java uses call by value exclusively. In the call-by-reference entry that follows, however, it writes that Java behaves as if it used call by reference for anything other than basic types.

Even though Java uses call-by-value exclusively, whenever we pass the name of an object to a called procedure, the value received by that procedure is in effect a pointer to the object. Thus, the called procedure is able to affect the value of the object itself. …​

As we noted when discussing call-by-value, languages such as Java solve the problem of passing arrays, strings, or other objects by copying only a reference to those objects. The effect is that Java behaves as if it used call-by-reference for anything other than a basic type such as an integer or real.

— Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman
Compilers: Principles, Techniques, and Tools, 2nd edition, Section 1.6.6

"Behaves as if it used call-by-reference" refers to the effect: the object is not copied as a whole, and changes the called method makes to the object’s state are visible to the caller. The phrase assumes what the book stated earlier, that the actual mechanism is not call by reference.

The word "reference" itself seems to have fed the misconception that Java passes objects by call by reference. Java calls a value that points to an object a "reference," and that word overlaps with the "reference" in "call by reference." Put more precisely, Java passes the reference value that points to an object by call by value.

A Java reference is a value that identifies and gives access to an object. You can think of it as a pointer. The following Java object access

Accessing a Java object
var myTv = new Tv();
myTv.setChannel("Rock");

is similar to this C++ code that uses a pointer.

C++ pointer
Tv *myTv = new Tv();
myTv->setChannel("Rock");

The Java variable myTv holds not the object itself but a value that points to the object. When you pass myTv as an argument to another method, this reference value is copied and passed. The C++ code above is an analogy for object access and pointer reassignment only; it does not mean that object lifetime management or the actual representation of references are the same.

Let’s look at the relevant wording in the JLS (Java Language Specification). The JLS also uses the word "pointer." Section 4.3.1. Objects defines reference values as pointers to objects.

The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.

— Java Language Specification
4.3.1. Objects

This describes language-level semantics; it does not guarantee that a reference value has the same format as a raw memory address. JVMS (Java Virtual Machine Specification) 2.7 does not mandate any particular internal structure for objects, and notes that in some implementations a reference may point to a handle rather than to the object data. A handle here is a small intermediate structure between the reference and the object data. The reference points to the handle, and pointers inside the handle in turn point to the object data and the method table. In other words, a reference value may not be the memory address where the object resides.

This description of handles originally described "Sun’s current implementation" in the first edition of the JVMS (1997). The second edition changed the wording to "some of Sun’s implementations," and editions after the Oracle acquisition to "some of Oracle’s implementations," but the content stayed the same. At the time of the first edition, Sun’s JVM was the Classic VM of JDK 1.0 and 1.1, and the "Handleless Objects" section of The Java HotSpot Performance Engine Architecture explains that the Classic VM used indirect handles. According to the same section, HotSpot, which today’s Oracle JDK and OpenJDK share, does not use handles and implements object references as direct pointers to objects.

How arguments are passed in a method call is described in Section 8.4.1. Formal Parameters. It says only that the "values" of the argument expressions initialize newly created parameter variables. Nothing in it says that a parameter becomes an alias of the caller’s variable, as call by reference was defined above.

When the method or constructor is invoked (§15.12), the values of the actual argument expressions initialize newly created parameter variables, each of the declared type, before execution of the body of the method or constructor.

— Java Language Specification
8.4.1. Formal Parameters

A New Frame for Every Method Invocation

The JLS also describes, step by step, how a method invocation is executed. Section 15.12.4.5. Create Frame, Synchronize, Transfer Control says that once the method to invoke has been determined, a new activation frame is created and the argument values are placed in it. An activation frame is a region of memory that holds the execution state of a method call, such as its parameters, local variables, and return address, and that disappears when the call ends.

Now a new activation frame is created, containing the target reference (if any) and the argument values (if any), as well as enough space for the local variables and stack for the method to be invoked …​ The effect of this is to assign the argument values to corresponding freshly created parameter variables of the method, and to make the target reference available as this, if there is a target reference.

— Java Language Specification
15.12.4.5. Create Frame, Synchronize, Transfer Control

The argument "values" are assigned to parameter variables freshly created in the new frame, not to the caller’s variables. This reaches the same conclusion as Section 8.4.1. The reference to the object on which an instance method is invoked (the target reference) is placed in the new frame in the same way and becomes this.

The JVMS defines this frame at the virtual machine level. Section 2.6. Frames says that a new frame is created on the thread’s JVM stack every time a method is invoked, and that each frame has its own array of local variables.

A new frame is created each time a method is invoked. …​ Frames are allocated from the Java Virtual Machine stack (§2.5.2) of the thread creating the frame. Each frame has its own array of local variables (§2.6.1), …​

— Java Virtual Machine Specification
2.6. Frames

Parameters are passed through this local variable array, as Section 2.6.1. Local Variables explains.

The Java Virtual Machine uses local variables to pass parameters on method invocation. On class method invocation, any parameters are passed in consecutive local variables starting from local variable 0. On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language). Any parameters are subsequently passed in consecutive local variables starting from local variable 1.

— Java Virtual Machine Specification
2.6.1. Local Variables

Let’s trace how the reference value of myTv in the earlier reassign example is copied into the parameter targetTv within this structure. Two concepts are needed first.

  • Operand stack: a last-in, first-out stack, one per frame, that serves as the workspace where JVM instructions pop operands and push results. It is also used to prepare the arguments passed to a method.

  • Bytecode: the JVM instructions that javac compiles Java source into and stores in class files. Two instructions appear in this example’s call sequence.

    • aload_1: pushes the reference value held in local variable 1 of the current frame onto the operand stack.

    • invokestatic: invokes a static method. It pops the argument values from the operand stack, creates a new frame, and stores those values in the new frame’s local variables starting from local variable 0.

With these concepts, the call to reassign looks like the figure below. Because reassign is a static method, the parameter targetTv is local variable 0 of the reassign frame. The reference value held by myTv, which javac assigned to local variable 1 of the main frame, is copied into this slot. The JVMS dictates that parameters start at slot 0, but the compiler decides which slot each non-parameter local variable occupies. At the bytecode level, the aload_1 instruction pushes this reference value onto the operand stack of the main frame, and the invokestatic instruction pops it and stores it in local variable 0 of the new frame. The two slots are separate storage belonging to different frames, so assigning a new object to targetTv leaves myTv in the main frame pointing to the original object.

The reassign frame and the main frame each have their own local variable array

Keep in mind that the JVMS defines an abstract machine. It is a model that specifies the rules for how instructions behave, not the memory layout of a particular implementation. The introduction to Chapter 2 of the JVMS leaves the memory layout of run-time data areas to the implementor’s discretion. For example, machine code produced by a JIT compiler may keep parameters in CPU registers. Even so, the language semantics, in which the caller’s variable and the parameter are separate variables, do not change.

Kotlin

Kotlin does not support call by reference either. In Kotlin, however, you cannot even run this article’s test of whether assigning a new value to a parameter changes the caller’s variable. The Function declaration section of the Kotlin Language Specification states that each parameter is introduced as the name of a value inside the function body, so parameters are final and cannot be changed inside the function.

Each parameter pi: Pi = vi introduces pi as a name of value with type Pi available inside function body b; therefore, parameters are final and cannot be changed inside the function.

— Kotlin Language Specification
Function declaration

Code that assigns a new value to a parameter is a compile error.

Reassigning a parameter and changing object state in Kotlin
class Tv(var channel: String)

fun main() {
    val myTv = Tv("Classic")
    changeState(myTv)
    println(myTv.channel) // Rock
}

fun reassign(targetTv: Tv) {
    targetTv = Tv("Rock") // (1)
}

fun changeState(targetTv: Tv) {
    targetTv.channel = "Rock" // (2)
}
  1. This is a compile error with the message "'val' cannot be reassigned."

  2. Changing a property of the object the parameter points to makes the new value visible to the caller. If you delete the reassign function that fails to compile and run the code, it prints Rock.

Apart from reassignment being forbidden, Kotlin behaves like Java. On the JVM, what is passed is a copy of the reference value, as in Java. Because final parameters are part of the language specification, this does not change when you compile to non-JVM platforms such as Kotlin/JS or Kotlin/Native.

JavaScript

JavaScript behaves the same way as Java. Here is the swap example first.

JavaScript swap function
function swap(x, y) {
  const temp = x;
  x = y;
  y = temp;
}

let x1 = "Hello";
let y1 = "World";
swap(x1, y1);
console.log(x1); // Hello

When an object is passed, reassigning the parameter or changing the object’s state inside the function also gives the same results as in Java.

Reassignment and state change when passing an object in JavaScript
const myTv = { channel: "Classic" };

reassign(myTv);
console.log(myTv.channel); // Classic

changeState(myTv);
console.log(myTv.channel); // Rock

function reassign(targetTv) {
  targetTv = { channel: "Rock" }; // (1)
}

function changeState(targetTv) {
  targetTv.channel = "Rock"; // (2)
}
  1. Assigning a new object to the parameter targetTv does not change the caller’s myTv.

  2. Changing a property of the object the parameter points to makes the new value visible through the caller’s myTv.

Replace the Tv object in the two Java figures above with an object literal, and they describe JavaScript’s behavior.

The Functions page on MDN summarizes this behavior in the same terms: arguments are always passed by value, and object arguments are, more precisely, passed by sharing.

Arguments are always passed by value and never passed by reference. This means that if a function reassigns a parameter, the value won’t change outside the function. More precisely, object arguments are passed by sharing, which means if the object’s properties are mutated, the change will impact the outside of the function.

— MDN Web Docs
Functions - Passing arguments

The ECMAScript specification also describes this behavior as initializing new parameter bindings with argument values, not as creating aliases of the caller’s variables. ArgumentListEvaluation obtains values from the argument expressions with GetValue and builds a list of ECMAScript language values, and FunctionDeclarationInstantiation initializes the parameter bindings of the new function environment from that list.

Unlike Java, the ECMAScript specification does not call the value used to access an object a "reference value." Object is one of the eight ECMAScript language types, the kinds of values a program manipulates directly, and an object itself is a value of that type. The "copied reference value" in this article is therefore best understood as a model for explaining the observed behavior.

TypeScript passes arguments the same way as JavaScript. The TypeScript Handbook states that, as a principle, TypeScript does not change the runtime behavior of JavaScript code.

As a principle, TypeScript never changes the runtime behavior of JavaScript code.

— TypeScript Handbook
TypeScript for the New Programmer - Runtime Behavior

After type checking, the TypeScript compiler erases the type information and emits plain JavaScript, so argument passing is identical to the JavaScript behavior described above. TypeScript also has no syntax for reference parameters like C#'s ref and out, which are covered later.

The By Reference Wording in JavaScript: The Definitive Guide

Some sources make it easy to mistake JavaScript function calls for call by reference if you read only part of them. JavaScript: The Definitive Guide, 4th edition, written by David Flanagan and long regarded as the standard JavaScript book, has a summary table in Section 11.2, "By Value Versus by Reference," stating that objects are copied, passed, and compared by reference. From that table alone, it looks as if you could describe JavaScript as call by reference.

Read the explanation of the behavior in the same section, however, and the book’s "by reference" turns out to mean something different from the call by reference defined at the beginning of this article. The book uses the phrase to mean that a reference to the object is passed instead of the whole object value being copied, and it also explains that reassigning the parameter is not reflected in the caller’s variable.

A function can use the reference to modify properties of the object or elements of the array. But if the function overwrites the reference with a reference to a new object or array, that modification is not visible outside of the function. Readers familiar with the other meaning of this term may prefer to say that objects and arrays are passed by value, but the value that is passed is actually a reference rather than the object itself.

— David Flanagan
JavaScript: The Definitive Guide, 4th edition, 11.2 By Value Versus by Reference

The example that follows this explanation is titled "References themselves are passed by value." The observed behavior the book describes is the same as in this article; only the name for what is passed differs.

C

C has no syntax like C++ reference parameters, so it has only call by value. Paragraph 4 of 6.5.2.2 Function calls in the C11 working draft N1570 states that, in preparing a function call, the arguments are evaluated and each parameter is assigned the value of the corresponding argument.

In preparing for the call to a function, the arguments are evaluated, and each parameter is assigned the value of the corresponding argument.

— C11 Working Draft N1570
6.5.2.2 Function calls, paragraph 4

Footnote 93, attached to the same paragraph, explains that a function may change the values of its parameters, but these changes cannot affect the values of the arguments; on the other hand, it is possible to pass a pointer to an object, and the function may then change the value of the object pointed to.

swap implemented with C pointer parameters
void swap(int* x, int* y) {
    int temp = *x;
    *x = *y; // (1)
    *y = temp;
}

int a = 1;
int b = 2;
swap(&a, &b); // (2)
  1. Writing to the variable the pointer points to changes the caller’s a. Assigning a different address to x, however, leaves the caller’s a unchanged.

  2. & is just the address-of operator, and copies of the pointer values are assigned to the parameters x and y. After the call, a is 2 and b is 1.

Applying the test from above, assigning a new value to a pointer parameter does not change the caller’s variable, so this is call by value. It has the same structure as Java passing a reference value to an object.

C++

C++ provides reference parameters, which are call by reference as this article defines it.

swap implemented with C++ reference parameters
void swap(int& x, int& y) { // (1)
    int temp = x;
    x = y; // (2)
    y = temp;
}

int a = 1;
int b = 2;
swap(a, b); // (3)
  1. Declaring the parameter with type int& makes x an alias of the caller’s variable a.

  2. An assignment inside the function actually changes the caller’s variable.

  3. Unlike C’s swap(&a, &b), nothing at the call site indicates that an address is passed. After the call, a is 2 and b is 1.

Pointer Parameters vs. Reference Parameters

C++ also has the pointer parameters (for example, int*) inherited from C, so the two need to be distinguished. Pointer parameters are call by value in C++ too. A function that takes pointers, such as void swap(int* x, int* y), receives copies of the pointer values in its parameters, exactly as in the C example. Assigning a different address to x inside the function is not reflected in the caller’s variable.

An int& reference parameter, on the other hand, has no syntax for reseating it at all. x = y; is not a statement that rebinds the reference to another variable; it assigns a value to the object the reference refers to. Applying the test from above to the two cases gives different results. Assigning to a pointer parameter (int*) leaves the caller’s variable unchanged, while assigning to a reference parameter (int&) changes it.

References in the C++ Standard

The C++ standard also describes a reference as an alias, not as a value that is passed. A note in [dcl.ref] says that a reference can be thought of as "a name of an object." Paragraph 4 of the same section explicitly leaves unspecified whether a reference requires storage.

It is unspecified whether or not a reference requires storage ([basic.stc]).

— C++ Working Draft N4950
[dcl.ref] paragraph 4

Even though compilers usually implement references as addresses, the standard does not even specify whether a reference has storage.

If you judge by what is copied at the machine level, no language would have call by reference at all. The two terms distinguish the semantics a language defines, not the compiled result.

That said, the C++ standard does not call reference parameters call by reference. Searching the entire standard turns up no occurrence of "call by value," "call by reference," or "pass by value," and only four occurrences of "passed by reference," all in notes or library clauses rather than normative definitions. Instead of naming evaluation strategies, the standard specifies behavior. Paragraph 6 of [expr.call] says only that each parameter is initialized with its corresponding argument.

When a function is called, each parameter ([dcl.fct]) is initialized ([dcl.init], [class.copy.ctor]) with its corresponding argument.

— C++ Working Draft N4950
[expr.call] paragraph 6

How a parameter of reference type is initialized is governed separately by the reference binding rules. Call by value and call by reference are not standard terms of any particular language; they are programming language theory terms for comparing argument-passing mechanisms across languages.

C#

Like C++, C# supports call by reference in its syntax. The Method parameters page of the C# reference states that C# passes arguments by value by default, and that a method receives a copy of the value for value types such as structs and a copy of the reference for reference types such as classes.

By default, C# passes arguments to functions by value. This approach passes a copy of the variable to the method. For value (struct) types, the method gets a copy of the value. For reference (class) types, the method gets a copy of the reference.

— C# reference
Method parameters and modifiers

The page goes on to explain that when a reference type is passed by value, reassigning the parameter inside the method does not change the caller’s variable, while changing an instance member is visible through the caller’s variable because both variables refer to the same instance. So far, this is the same as Java.

To pass a parameter by reference, you add the ref, out, in, or ref readonly modifier. Of these, only ref and out allow assigning to the caller’s variable. The same page explains that a parameter passed by reference has no value of its own; it is a reference variable that refers to another variable, the referent. This is call by reference as defined in this article: the parameter becomes an alias of the caller’s variable.

Swap implemented with C# ref parameters
void Swap(ref int x, ref int y) // (1)
{
    int temp = x;
    x = y; // (2)
    y = temp;
}

int a = 1;
int b = 2;
Swap(ref a, ref b); // (3)
  1. The parameter x, declared with the ref modifier, becomes an alias of the caller’s variable a.

  2. Assigning to the parameter changes the caller’s variable.

  3. Unlike C++, the call site also needs ref. After the call, a is 2 and b is 1.

Because a ref parameter requires ref at both the method declaration and the call site, you can tell from the call expression alone that the caller’s variable may change, unlike with C++ reference parameters. out is a variant in which the caller passes an uninitialized variable and the method must assign a value to it, and in and ref readonly are read-only references that the method cannot modify.

ref also works with variables of reference types. Passing a class instance as is copies the reference, as in Java, so reassigning the parameter is not reflected in the caller’s variable. Passing it with ref makes the parameter an alias of the caller’s variable, so reassignment can change which object the caller’s variable points to.

Replacing the caller’s reference-type variable with a C# ref parameter
var myTv = new Tv();
myTv.Channel = "Classic";

Replace(ref myTv); // (1)
Console.WriteLine(myTv.Channel); // Rock

void Replace(ref Tv targetTv)
{
    targetTv = new Tv(); // (2)
    targetTv.Channel = "Rock";
}

class Tv
{
    public string Channel { get; set; } = "";
}
  1. If you declare it as Replace(Tv targetTv) without ref and call Replace(myTv), it behaves like Java’s reassign(myTv) and prints Classic. If the declaration has ref and you omit only the ref at the call site, it is a compile error.

  2. Assigning a new object to the ref parameter makes the caller’s myTv point to the new object. Running it on .NET 8 prints Rock.

C# distinguishes in its syntax between "passing a reference type by value" and "passing a variable by reference with ref," two different behaviors.

Python

Like Java, Python has only call by value. Section 4.8 Defining Functions of the official tutorial explains that arguments are introduced into the local symbol table of the called function, so they are passed using call by value, where the value is always an object reference.

The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object).

— The Python Tutorial
4.8 Defining Functions

A footnote on this sentence adds that "call by object reference" would be a better description, because when a mutable object is passed, the caller sees any changes made to it. The Python FAQ answers more directly: arguments are passed by assignment, and since assignment just creates references to objects, there is no alias between an argument name in the caller and in the callee, and so no call by reference.

Python swap function and passing an object
def swap(x, y):
    x, y = y, x  # (1)

a, b = 1, 2
swap(a, b)
print(a)  # 1


class Tv:
    def __init__(self, channel):
        self.channel = channel


def main():
    my_tv = Tv("Classic")
    reassign(my_tv)
    print(my_tv.channel)  # Classic
    changeState(my_tv)
    print(my_tv.channel)  # Rock


def reassign(targetTv):
    targetTv = Tv("Rock")  # (2)


def changeState(targetTv):
    targetTv.channel = "Rock"  # (3)


main()
  1. This only rebinds the local names x and y to different objects, so the caller’s a stays 1.

  2. Rebinding the parameter targetTv to a new object leaves the caller’s my_tv unchanged, and Classic is printed.

  3. Changing an attribute of the object targetTv points to makes the change visible through the caller’s my_tv, and Rock is printed.

The results are the same as Java’s Tv example and JavaScript’s reassign and changeState examples.

Because the caller’s variable stays the same when you pass an integer or a string, but appears changed when you pass a list or a dictionary, it is easy to assume the passing rule depends on the type. The rule is the same. Integers and strings are immutable objects, so there is simply no way to change their internal state. Even with a list, rebinding the parameter inside the function, as in x = [1], leaves the caller unchanged.

Go

Go does not make the caller’s variable an implicit alias of a parameter in a function call either. The Calls section of the Go specification says that after the function value and arguments are evaluated, new storage is allocated for the function’s variables, including its parameters and results, and the arguments are assigned to the corresponding parameters. The Go FAQ answers more directly.

As in all languages in the C family, everything in Go is passed by value. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter.

— Go FAQ
When are function parameters passed by value?

Taken literally, "all languages in the C family" is not accurate. C++ and C#, covered in this article, belong to the C family but support reference parameters in their syntax. Both languages still pass by value by default, though, and passing by reference is an exception that requires a modifier. The sentence is best read as "the default in C-family languages is pass by value, and Go has only that default." As in C, Go requires you to pass a pointer explicitly to change the caller’s variable.

swap implemented with Go pointer parameters
func swap(x, y *int) {
    *x, *y = *y, *x // (1)
}

a, b := 1, 2
swap(&a, &b) // (2)
  1. Writing to the variables the pointers point to changes the caller’s a and b. Assigning a different pointer to x inside the function does not change the caller’s a.

  2. To change the caller’s values with an ordinary function, you pass pointers as in C. Copies of the pointer values are assigned to the parameters x and y. After the call, a is 2 and b is 1.

Method calls, however, do not need & at the call site. The Calls section of the Go specification states that if x is addressable and the method set of &x contains m, then x.m() is shorthand for (&x).m(). For methods with pointer receivers, the compiler takes the address for you under this rule, so you cannot tell from the shape of the call expression alone whether the caller’s variable may change. Even then, what is passed is a pointer value.

Rust

Rust does not support call by reference in the sense defined in this article, a mechanism in which the parameter becomes an alias of the caller’s variable. Rust by Example calls passing a &T "passed by reference." A function call, however, does not create an alias of the caller’s variable, and a reference such as &mut T is itself a value that is passed by call by value. The Rust Book explains that passing a variable to a function moves or copies the value, just as assignment does.

Passing a variable to a function will move or copy, just as assignment does.

— The Rust Programming Language
4.1 What is Ownership? - Ownership and Functions

To understand "move" in this explanation, you need Rust’s concept of ownership. The Ownership Rules section of the Rust Book summarizes ownership in three rules: each value has a variable that is its owner, there can be only one owner at a time, and when the owner goes out of scope, the value is dropped. An assignment such as let s2 = s1; transfers ownership to s2, which is called a move, and using s1 after the move is a compile error.

Which types are copied is determined by the Copy trait. A trait declares behavior shared by multiple types, similar to an interface in other languages. Some traits have no methods and only tell the compiler about a property of the type; Copy is one of them.

Copy is a trait for types whose original variable remains usable after its value is assigned to another variable. The Stack-Only Data: Copy section of the Rust Book explains that variables of types implementing Copy are not moved but trivially copied, so they remain valid after assignment to another variable. Integers, floating-point numbers, bool, char, and tuples containing only Copy types belong to this group. Types that allocate heap memory or own resources, such as String and Vec, cannot be Copy, and adding Copy to a type that implements Drop, which performs cleanup when a value goes out of scope, is a compile error. In effect, only types that are safe to duplicate bit for bit can be Copy. So in let t = s;, if s is an i32 you can keep using s afterward, but if it is a String it is moved, as shown above.

A value of a non-Copy type such as String has its ownership moved into the parameter, and a value of a Copy type such as i32 is copied. When you pass a variable of type &mut T to a parameter of type &mut T, however, the compiler implicitly reborrows it, so you can use the variable again after the call even though &mut T is not Copy.

From this article’s perspective, moving and copying are the same mechanism, in which the parameter is initialized with the argument value, and neither creates an alias of the caller’s variable. The only difference is whether the caller’s variable remains usable after the call.

swap implemented with Rust mutable references
fn swap(x: &mut i32, y: &mut i32) { // (1)
    let temp = *x;
    *x = *y; // (2)
    *y = temp;
}

let mut a = 1;
let mut b = 2;
swap(&mut a, &mut b); // (3)
  1. What is passed to the parameter x is a value of type &mut i32.

  2. Writing to the variable the mutable reference points to changes the caller’s a.

  3. To change the caller’s values with an ordinary function, you pass mutable references like this. After the call, a is 2 and b is 1.

The standard library function std::mem::swap uses the same approach with the signature fn swap<T>(x: &mut T, y: &mut T). Passing mutable references is still not call by reference as defined in this article, in which reassigning the parameter itself changes the caller’s variable binding. To reassign x, you have to declare the parameter as mut x: &mut i32, and even then the reassignment has no effect on the caller’s a.

Like Go’s pointer-receiver methods, method calls in Rust do not need &mut at the call site. Under the method call rules in the Rust Reference, writing v.push(1) makes the compiler automatically borrow the receiver and pass &mut v, and what is passed is still a mutable reference value.

Ada: Call by Reference vs. Copy-Restore

The definitions section noted that the test "when a function assigns a new value to its parameter, does the caller’s variable change?" cannot classify every passing mechanism by itself. Ada is an example. Ada declares parameters with the modes in, in out, and out, and a value assigned to an in out or out parameter is reflected in the caller’s variable. Judged only by the result the caller observes, this is the same as C++ reference parameters.

Ada Reference Manual 6.2, however, decides whether a parameter is passed by copy or by reference mainly by its type, not its mode. Elementary types such as Integer are by-copy types and are passed by copy; by-reference types such as tagged types are passed by reference; and a formal parameter declared aliased is passed by reference regardless of its type. For parameters that fall into neither category, the manual does not specify whether they are passed by copy or by reference and leaves the choice to the implementation. For in out and out parameters passed by copy, 6.4.1 specifies that their values are copied back to the actual arguments when the subprogram completes normally. This is the call by copy-restore mentioned in the definitions section. The result, a changed caller variable, is the same as with call by reference, but the actual mechanism is copying.

The difference between passing by copy and passing by reference becomes visible when the procedure reads the variable passed as the actual argument directly. The example below passes the same variable Original to a procedure that receives it by copy and to one that receives it as aliased, and reads Original right after assigning to the parameter.

By-copy passing vs. aliased by-reference passing in Ada
with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is
   Original : aliased Integer := 1;

   procedure By_Copy (Param : in out Integer) is
   begin
      Param := 2;
      Put_Line ("inside By_Copy: Original =" & Integer'Image (Original)); -- (1)
   end By_Copy;

   procedure By_Reference (Param : aliased in out Integer) is
   begin
      Param := 2;
      Put_Line ("inside By_Reference: Original =" & Integer'Image (Original)); -- (3)
   end By_Reference;
begin
   By_Copy (Original);
   Put_Line ("after By_Copy: Original =" & Integer'Image (Original)); -- (2)

   Original := 1;
   By_Reference (Original);
   Put_Line ("after By_Reference: Original =" & Integer'Image (Original));
end Demo;
  1. Integer is a by-copy type, so Param is a copy of Original. Right after 2 is assigned to Param, Original is still 1.

  2. When the procedure completes normally, the value of Param is copied back to Original, and Original becomes 2.

  3. An aliased parameter is passed by reference, so Param is an alias of Original. The moment 2 is assigned to Param, Original is 2 as well.

This is the output when compiled with GNAT 13.3, the Ada compiler included in GCC.

Output
inside By_Copy: Original = 1
after By_Copy: Original = 2
inside By_Reference: Original = 2
after By_Reference: Original = 2

After both calls return, the value is 2 in both cases, but reading Original inside the procedures gives different results. A language supporting parameter modes in its syntax and those modes being implemented as call by reference are two separate facts.

Summary

The following table summarizes how the languages covered in this article pass arguments.

Language Supports call by reference Passing mechanism Changing the caller’s variable through a parameter

Java

No

Call by value. For objects, a copy of the reference value is passed

Not possible. Only object state changes are shared

Kotlin

No

Call by value. Parameters are final, so reassignment is a compile error

Not possible. Only object state changes are shared

JavaScript

No

Call by value. The Object value is assigned to a separate parameter binding

Not possible. Only object state changes are shared

C

No

Call by value

Pass a pointer (swap(&a, &b))

C++

Yes, only for reference parameters (int&)

Call by value by default. Only reference parameters are call by reference

Declare a reference parameter. You can also pass a pointer as in C

C#

Yes, with ref, out, in, and ref readonly parameters. Of these, ref and out allow assigning to the parameter

Call by value by default. For class instances, a copy of the reference is passed

Declare a ref or out parameter. The call site also needs ref or out

Python

No

Call by value. The value is always an object reference

Not possible. Only object state changes are shared

Go

No

Call by value. Map and slice values behave like pointers

Pass a pointer (swap(&a, &b)). Pointer-receiver methods take the address automatically

Rust

No

Call by value. Depending on the type, the value is moved or copied as in assignment, and a mutable reference &mut T is also passed by value

Pass a mutable reference (swap(&mut a, &mut b)). Method calls borrow the receiver automatically

Ada

Partially. By-reference types and parameters declared aliased are passed by reference regardless of mode. For other types, the manual does not specify. in out and out parameters of by-copy types are copied back on normal completion (copy-restore)

By copy or by reference, depending on the type

Declare an in out or out mode

  • Java, Kotlin, JavaScript, and Python initialize a separate parameter variable or binding with the argument value. They do not support call by reference, in which the caller’s variable itself becomes an alias of the parameter.

    • Passing an object neither clones the object nor passes the caller’s variable itself. In Java, the reference value that points to the object, and in JavaScript, the Object value, is passed to a separate parameter, and the caller and callee can observe the same object.

    • Reassigning a parameter inside a function does not change the caller’s variable. Changing the internal state of the object the parameter points to, on the other hand, is visible through the caller’s variable. This difference is the source of the misconception that "objects are call by reference."

  • C and Go pass pointers, and Rust passes mutable references, to change the caller’s values. A call that takes the address or reference of a variable on the spot shows & or &mut, but when you pass a variable that already holds a pointer or reference, or when the compiler takes the address or borrows for you as in Go and Rust method calls, you cannot tell from the shape of the call expression alone. The only languages here in which a parameter becomes an alias of the caller’s variable are C++, C#, and Ada, and Ada sometimes passes by copy and copies back depending on the type.

  • This object-sharing behavior is sometimes called call by sharing. Some sources, such as JavaScript: The Definitive Guide, 4th edition, describe object sharing as "by reference," but it should be distinguished from call by reference in the sense of passing an alias of the caller’s variable.

References

VO vs. DTO: Definitions and the History of Conflating the Terms

Calling a getter/setter data carrier a VO (Value Object) causes confusion. A Value Object is a classification by meaning and value equality, while a DTO is a classification by its role of transferring data. This article walks through both definitions, how Core J2EE Patterns spread the conflation in the Java/J2EE world, and a naming convention that keeps roles visible.

In practice, you often come across objects with nothing but getters and setters, whose job is to carry values, being called VOs (Value Objects). If the object’s role is to carry data across a remote call between processes, calling it a DTO (Data Transfer Object) leaves less room for confusion. Value Object is a term that Martin Fowler’s writing and Eric Evans’s DDD (Domain-Driven Design) have already defined with a different meaning. This article lays out the definitions of the two patterns and the history of how the terms got mixed up.

Definition of Value Object

A Value Object is an object that has no identity and whose equality is determined by the values it holds.

Identity is the property that distinguishes one object from another regardless of its attribute values. A member is still the same member after their name or address changes, and two members with the same name and address are still different people. Such objects are distinguished by an identifier like a member number, and DDD calls them ENTITIES.

Equality is the criterion for deciding whether two objects count as the same. A Value Object bases this criterion on the values it holds rather than on an identifier. Concepts like money, color, and date are typical examples. For instance, Instant in Java, which represents a point on the timeline, compares equal with equals() when two instances refer to the same instant, even if they were created separately by different means.

Instant fromText = Instant.parse("2026-09-24T20:00:00Z");
Instant fromSeoulTime = ZonedDateTime.of(2026, 9, 25, 5, 0, 0, 0, ZoneId.of("Asia/Seoul"))
    .toInstant();

assertThat(fromText).isEqualTo(fromSeoulTime); // 20:00 on the 24th UTC and 05:00 on the 25th in Seoul are the same instant

This definition can be confirmed in the three sources below.

  • Martin Fowler’s article: objects regarded as equal because their attribute values are equal. He explains this using a point made up of x and y coordinates as the example.

    Objects that are equal due to the value of their properties, in this case their x and y coordinates, are called value objects.

  • Wikipedia: an object whose equality is not based on identity, and which counts as the same when it holds the same values

  • Microsoft’s .NET architecture documentation: an object with no identity

Eric Evans takes the same view. In the Value Objects entry of the Domain-Driven Design Reference (2015), which Evans published as a summary of the pattern definitions in Domain-Driven Design, the problem statement presupposes that many objects have no conceptual identity, and it says to classify a model element as a value object when you care only about its attributes and logic.

Some objects describe or compute some characteristic of a thing. Many objects have no conceptual identity. …​

Therefore:

When you care only about the attributes and logic of an element of the model, classify it as a value object. Make it express the meaning of the attributes it conveys and give it related functionality.

— Eric Evans
Domain-Driven Design Reference: Value Objects

In Java code, a class that expresses a value concept becomes a Value Object when it implements equals() and hashCode() on the attributes that serve as the criterion for equality. The contracts of these two methods that must be honored are laid out in Item 10 and Item 11 of Joshua Bloch’s Effective Java, 3rd ed.

  • Item 10: the general contract of equals()

    • Reflexive: x.equals(x) returns true.

    • Symmetric: if x.equals(y) returns true, then y.equals(x) also returns true.

    • Transitive: if x.equals(y) and y.equals(z) return true, then x.equals(z) also returns true.

    • Consistent: as long as the information used in the comparison does not change, x.equals(y) returns the same result no matter how many times it is called.

    • Non-null: x.equals(null) returns false.

  • Item 11: when you override equals(), also override hashCode()

    • As long as the information used in equals() comparisons does not change, hashCode() returns the same value no matter how many times it is called.

    • Two objects that are equal according to equals() return the same hashCode() value.

    • Two objects that are unequal according to equals() are not required to return different hashCode() values, but returning different values improves the performance of hash tables.

With records, which became a standard feature in Java 16, you no longer have to write these two methods yourself. JEP 395, which introduced records, states that a record’s equals() and hashCode() are generated automatically based on the values of all its components. Two record instances are equal when they have the same type and all of their component values are equal.

public record Money(BigDecimal amount, Currency currency) {
}

Two instances of this class are equal when the values they hold are the same.

Currency krw = Currency.getInstance("KRW");
Money price1 = new Money(BigDecimal.valueOf(10000), krw);
Money price2 = new Money(BigDecimal.valueOf(10000), krw);

assertThat(price1).isEqualTo(price2); // equal when the values match, even with different references

However, because a record uses the equals() of each component type as is, the automatically generated equality may differ from the equality the domain wants. For example, BigDecimal’s `equals() compares the scale as well, so Money instances created from new BigDecimal("10000") and new BigDecimal("10000.0") end up as different values. In such cases, normalize the values in the constructor or define equals() yourself.

The concept of an identity-free value object is also being introduced into the Java language and the JVM. It shares with the Value Object of DDD and Fowler the trait of being distinguished by value, without identity. Project Valhalla’s JEP 401: Value Objects (Preview) proposes value objects that are immutable, have no object identity, and are distinguished only by their field values. As of August 2026, this JEP has been integrated as a preview feature into JDK 28, scheduled for release in March 2027. JEP 169: Larval State for Value Objects is a separate Draft proposal that deals with a temporary mutable state for these immutable value objects. That said, identity in DDD is a question of whether something needs to be conceptually distinguished in the domain, while the identity that Valhalla removes is a runtime property by which the JVM tells objects apart, so the two value objects are not the same concept. Even so, neither side uses value object to mean an object with nothing but getters and setters.

Meanwhile, in practice, a convention has also spread that interprets VO in the broad sense of a data holder, independent of the definition above, and calls carrier objects with nothing but getters and setters VOs. The main source that spread this convention is examined in the Core J2EE Patterns section below.

The Status of Immutability: Definitional Requirement vs. Good Design

Many books and articles recommend making Value Objects completely immutable.

  • On p. 486 of Patterns of Enterprise Application Architecture, Fowler recommends making Value Objects immutable, saying "it’s a very good idea to make them immutable".

  • Fowler’s Value Object article says the same. The version before the 2016 revision presented making value objects entirely immutable as a general heuristic, and the current revised version also presents "value objects should be immutable" as an important rule.

    A general heuristic is that value objects should be entirely immutable.

  • In the Value Objects entry of the DDD Reference quoted above, Eric Evans gives the design guideline to treat value objects as immutable.

    Treat the value object as immutable. Make all operations Side-effect-free Functions that don’t depend on any mutable state. Don’t give a value object any identity and avoid the design complexities necessary to maintain entities.

  • In Item 17 "Minimize mutability" of Effective Java, 3rd ed., Joshua Bloch recommends, for classes in general and not just Value Objects, minimizing mutability and making them immutable where possible.

If a VO is immutable, no aliasing bug arises even when several objects share the same instance. An aliasing bug is the problem where changing a value on one side also changes the value on another side that references the same instance. Java’s java.util.Date and Calendar are objects with the nature of values, yet they were designed to be mutable and became a source of such bugs. For example, when two objects share a single Date instance holding a meeting’s start time, calling setTime() on one side changes the start time on the other side as well.

Side effect of the mutable Date class
record Meeting(String title, Date start) {
}

Date start = new Date();
Meeting review = new Meeting("Design review", start);
Meeting retro = new Meeting("Retrospective", start);

long oneHourLater = start.getTime() + Duration.ofHours(1).toMillis();
review.start().setTime(oneHourLater); // (1)

assertThat(retro.start().getTime()).isEqualTo(oneHourLater); // (2)
  1. Tries to postpone only the design review by one hour

  2. The retrospective’s start time changes too

Declaring Meeting as a record is not enough on its own to prevent this problem. A record only prevents assigning a different instance to a field; the internal state of the Date that the field references can still be changed.

The fact that Java 8’s java.time package made all of its date and time classes, such as LocalDate and Instant, immutable also reflects this lesson. If the start time is represented as a LocalDateTime, a date and time without a time zone, plusHours() returns a new instance instead of modifying the existing one, so sharing the instance does not change the start time of the other meeting.

record Meeting(String title, LocalDateTime start) {
}

LocalDateTime start = LocalDateTime.of(2026, 9, 25, 14, 0);
Meeting review = new Meeting("Design review", start);
Meeting retro = new Meeting("Retrospective", start);

Meeting delayedReview = new Meeting(review.title(), review.start().plusHours(1));

assertThat(delayedReview.start()).isEqualTo(LocalDateTime.of(2026, 9, 25, 15, 0));
assertThat(retro.start()).isEqualTo(start); // (1)
  1. The retrospective’s start time is unchanged

A close look at Fowler’s and Evans’s sentences, however, shows that immutability appears as a guideline, not as the definition of a VO. Fowler presents immutability as a rule for avoiding aliasing bugs, and Evans presents it as an imperative design guideline. The property Fowler presents as the definition of a Value Object is equality by value. The "value objects should be immutable" quoted above also appears, when you read the whole sentence, as a rule he follows in order to avoid aliasing bugs.

To avoid aliasing bugs I follow a simple but important rule: value objects should be immutable.

— Martin Fowler
Value Object

In the same article, he even mentions an alternative: aliasing bugs can also be avoided when the language copies the value on every assignment, as with structs in C#.

While immutability is my favorite technique to avoid aliasing bugs, it’s also possible to avoid them by ensuring assignments always make a copy. Some languages provide this ability, such as structs in C#.

— Martin Fowler
Value Object

Evans wrote about immutability in imperative sentences. In the solution part of the DDD Reference quoted above, the criterion for classifying something as a value object is whether you care only about the attributes and logic of the model element. The "Treat the value object as immutable." that follows is an imperative sentence telling you to treat the objects so classified as immutable, and the sentence right after it is also an imperative, telling you to make all operations side-effect-free functions. I read these sentences not as classification criteria but as design guidance for handling the objects once they have been classified.

The comments Fowler left on the ValueObjectsShouldBeImmutable page of Ward Cunningham’s wiki show the distinction between definition and guideline even more clearly. He says not to give a newly designed object any methods that change its state.

So if you design an object that should be a value object, don’t provide any methods that change its state, ie make it immutable.

— Martin Fowler
c2 wiki: ValueObjectsShouldBeImmutable

And about Value Objects that have already been made mutable, he says the following.

If you are using a ValueObject that is mutable, treat it like it is immutable. You may not realize why, but you will save a lot of time and money.

— Martin Fowler
c2 wiki: ValueObjectsShouldBeImmutable

The very premise "If you are using a ValueObject that is mutable" presupposes the existence of Value Objects that are not immutable. The page is also named ShouldBeImmutable, not MustBeImmutable. The Cambridge Dictionary gives the first meaning of should as follows.

used to say or ask what is the correct or best thing to do

— Cambridge Dictionary
should

The same dictionary explains must as used to show that it is necessary or very important that something happens. If must expresses a necessity that something has to be so, should expresses a recommendation that doing so is the right thing.

On the other hand, some sources do describe immutability as part of the definition.

  • Microsoft’s .NET architecture documentation mentioned above lists the absence of identity and immutability side by side as the two main characteristics of value objects, and states that immutability is an important requirement.

    There are two main characteristics for value objects: They have no identity. They are immutable. The first characteristic was already discussed. Immutability is an important requirement.

  • Wikipedia writes that value objects should be immutable, and explains that immutability is required for the implicit contract that two value objects created with the same values must remain equal. Although it uses should, it treats immutability as the premise of a contract, which gives it a status close to that of a definition.

  • Chapter 6 of Vaughn Vernon’s Implementing Domain-Driven Design (2013) also includes immutability as one of the characteristics it lists for Value Objects.

  • Kim Woo-geun’s Pragmatic Programming for Java/Spring Developers (2024, in Korean) also explains that "A VO is an object that has this property of immutability" (p. 43), and defines a VO as an object that satisfies three characteristics: immutability, equality, and self-validation.

  • There are also concepts like the value object of JEP 401, where fields implicitly become final and shallow immutability is enforced at the language level. A different instance cannot be assigned to a field, but the internal state of the object a field references does not thereby become immutable.

I see immutability as a desirable design norm rather than a definitional requirement of a VO. On the c2 wiki, Fowler too wrote that newly designed objects should be made immutable. Since a new Value Object is designed to be immutable whichever view you follow, cases where the difference between should and must shows up in practice are not common. Still, the distinction is not meaningless. When you meet a value-like object that was built mutable, like java.util.Date, including immutability in the definition makes that object something other than a VO, while treating immutability as a guideline makes it a VO that fails to follow the immutability guideline. Fowler’s sentence on the c2 wiki is practical advice from the latter viewpoint. It means that if you have already run into a VO that fails the immutability guideline, you should at least treat it as if it were immutable instead of excluding it as not a VO.

It is similar to the difference between including 'not driving after drinking' in the definition of a driver and seeing it as a norm a driver must follow. Either way, the conclusion that you must not drive after drinking is the same, but if the norm is folded into the definition, there is no longer a name for the violations that exist in reality, which makes such cases hard to discuss.

Definition of DTO (Data Transfer Object)

In Fowler’s original definition, a DTO is an object meant to reduce the cost of remote calls. The catalog for Martin Fowler’s Patterns of Enterprise Application Architecture defines a DTO as follows.

An object that carries data between processes in order to reduce the number of method calls.

— Martin Fowler
Patterns of Enterprise Application Architecture catalog: Data Transfer Object

The same definition appears on page 401 of the book. Every remote call costs a network round trip and serialization, so the pattern grew out of the intent to deliver all the needed data in a single call. For example, instead of fetching a customer’s name, address, and order list with three remote getter calls, you receive a single DTO holding all three values in one call.

Objects that hold data crossing the network, such as HTTP API requests and responses, fit this definition in that they cross a process boundary and are serialized. Not every HTTP API is designed to reduce the number of remote calls, however, so calling these objects DTOs extends the original definition to modern API boundaries.

In practice the definition has been stretched further. A convention has emerged of calling any object that carries data across layer boundaries within the same process a DTO, with no remote call involved. Examples include objects holding DB query results, objects passed from the service layer to the view rendering layer, and response-only objects converted from JPA entities so that the entities are not exposed directly outside the service layer. This usage is far from the original definition of reducing the number of remote calls, so one could argue that such objects are hard to call DTOs.

Apart from the debate over the name, one can also ask whether having such objects in a local context is desirable from a design standpoint. In LocalDTO, Fowler takes issue not with the name but with this usage. His starting point is that the DTO pattern exists to reduce the cost of remote calls, so in a local context with no remote calls that reason disappears. He therefore says that in a local context DTOs are not just unnecessary but actually harmful. The reasons are that a coarse-grained API that exchanges a lot of data in a single call is awkward to use, and that all the work of moving data from the domain layer or data source layer into DTOs is added cost.

Not just do you not need them in a local context, they are actually harmful both because a coarse-grained API is more difficult to use and because you have to do all the work moving data from your domain or data source layer into the DTOs.

— Martin Fowler
LocalDTO

To the argument that DTOs should be placed in the service layer API so that the service layer’s clients do not depend on the domain model, he replies that this may be convenient but is not worth all the cost of the data mapping.

In the same article, however, he acknowledges that something like a DTO is useful even locally when there is a large gap between the presentation layer’s model and the domain model.

One case where it is useful to use something like a DTO is when you have a significant mismatch between the model in your presentation layer and the underlying domain model.

— Martin Fowler
LocalDTO

In such cases the mapping between the two models is needed anyway, so the DTO is not an added cost. Later in the same article he also adds the use of DTOs for exchanging data as messages between isolated areas in a multithreaded application. In short, Fowler’s criticism is aimed at DTOs introduced for the sake of separation from the domain model itself. It does not reject cases where there is a real need, such as a mismatch between models or message passing between isolated areas.

The naming debate and the usage debate are different questions, but they start from the same premise. The fact that a local context has no remote calls serves, on one side, as grounds that the name DTO does not match the original definition, and on the other, as grounds that the reason to have a DTO grows weak. The role-based suffixes I propose later are an answer to the naming debate. Whether to have such an object at all is a matter to settle separately in the usage debate, and if you decide to have one, the proposal is to give it a name that reveals its role rather than Dto.

In the end, the criterion for distinguishing a VO from a DTO is the object’s characteristics and role. Representing a value concept and judging equality by value is a property of a VO; carrying data across a boundary is the role of a DTO. The two are not mutually exclusive categories. An immutable DTO with value equality is also a VO. Conversely, not every DTO needs value equality.

VO in the First Edition of Core J2EE Patterns and TO in the Second

In the Java/J2EE (now Jakarta EE) community, a prominent early source that spread the conflation of the two terms is Core J2EE Patterns: Best Practices and Design Strategies by Deepak Alur and others. The first edition, published in 2001, defined an object that transfers data between tiers as a pattern named Value Object. The second edition in 2003 renamed the same pattern TO (Transfer Object). The rename appears intended to avoid confusion with the Value Object as a value-equality object. Martin Fowler calls the pattern with the same role a DTO (Patterns of Enterprise Application Architecture, p. 401). In short, the following three refer to the same transfer pattern.

VO in Core J2EE Patterns 1st ed. = TO in the 2nd ed. = Martin Fowler’s DTO

Oracle’s Transfer Object documentation describes this pattern under the renamed name, Transfer Object.

Several other books also wrote that VO and DTO mean the same thing or are very close concepts.

  • Rod Johnson, Expert One-on-One J2EE Design and Development, Wrox, 2002, p. 265

    Value objects are sometimes referred to as Data Transfer Objects (DTOs).

  • Rod Johnson and Juergen Hoeller, Expert One-on-One J2EE Development without EJB, Wrox, 2004, p. 27

    Transfer objects, often referred to as Data Transfer Objects (DTOs) or Value Objects.

  • Murat Yener and Alex Theedom, Professional Java EE Design Patterns, Wrox, 2014, Chapter 12

    The DTO is also referred to as the Value Object

  • Derek C. Ashmore, The Java EE Architect’s Handbook, Second Edition, DVT Press, 2014, Chapter 5

    My definition of 'value object' is very close to a Data Transfer Object (DTO)

I cannot say for certain that all of these books were directly influenced by the first edition of Core J2EE Patterns. But given when they appeared and the phrasing repeated across the literature, I suspect the first edition’s usage had some influence on the wording of later Java/J2EE literature. More than twenty years after the second edition changed the name, the practice remains.

The Cost of Calling a Data Carrier a VO

You might wonder whether a name already in common use within a team really needs to change. But the practice of calling data carrier objects VOs has a real cost, because the Value Object as a value-equality object keeps appearing in three contexts: DDD, ORM, and Java language and JVM features.

  • DDD (Domain-Driven Design): the VALUE OBJECT is, along with the ENTITY, a core building block of the domain model. An AGGREGATE is a consistency boundary for data changes with a single ENTITY as its root, and it can contain other ENTITIES and VALUE OBJECTS inside it.

  • ORM: @Embeddable in Hibernate and JPA maps an object that has no persistent identity of its own and belongs to the entity that owns it. It is the structure commonly used to map DDD VALUE OBJECTS, but JPA does not enforce value equality or immutability, so not every @Embeddable is a DDD VALUE OBJECT.

  • Java language and JVM features: the Project Valhalla documents, including JEP 401, use the term value object to mean an object that is immutable, has no object identity, and is distinguished by its field values.

If you understand a VO as a data holder with nothing but getters and setters, the terminology clashes when you read these materials, and confusion follows. For example, reading an explanation that says to map a VO as an @Embeddable, you picture a request parameter object full of setters. Calling a data carrier object that crosses a remote process boundary a DTO connects naturally both with Fowler’s definition and with the renamed name used since the second edition of Core J2EE Patterns.

As seen in the DTO definition section above, recent convention also uses DTO in the broad sense of a data holder that crosses a layer boundary. But attaching the DTO suffix to every carrier object has a drawback. If objects holding HTTP request parameters and objects holding the results of statistics queries are all called DTOs, the name alone does not reveal the role. For example, from the name IssueDto alone you cannot tell whether it is a request body, a lookup response, or the result of a statistics query.

I recommend dividing suffixes by the object’s role. The name then reveals the role on its own, and you also avoid the debate over departing from the strict definition when objects used in layers unrelated to remote calls are called DTOs. Here are some example names.

Role Example class names

JSON response for an issue lookup

IssueResponse, IssueDetailDto

JSON request for issue creation

IssueCreationRequest, IssueCreationCommand

Issue lookup criteria

IssueQuery, IssueCriteria

Result of an issue statistics query against the DB

IssueStatsRow

References

Transfer Object / DTO

Documenting and Verifying Thread Safety: Javadoc, Annotations, Static Analysis, and ArchUnit

How to state thread safety in Javadoc, and how to use annotations, static analysis tools, and ArchUnit tests to keep code that is unsafe under multiple threads from being deployed.

Whether instances of a class can be shared by multiple threads is information developers need to check carefully. Yet the standard Javadoc tags (@param, @return, @throws, and so on) include no tag for thread safety. As a result, users of a class can easily miss what its provider intended. This article first looks at a good classification scheme for stating thread safety in Javadoc. It then covers how to use annotations, static analysis tools, and ArchUnit tests to keep code that is unsafe under multiple threads from being deployed.

Thread Safety in Javadoc

This section covers four topics in order:

  • Why Javadoc does not show synchronized on methods

  • Why a class-level statement is still needed

  • A suitable classification for documentation

  • Examples from real library documentation

Why Javadoc Hides synchronized

When an instance method carries the synchronized keyword, the instance itself becomes the lock. Even if several threads call synchronized methods on the same instance at the same time, only one thread can enter. The others wait until the first thread leaves the method. A static synchronized method uses the Class object of its class as the lock. So synchronized is a clue to thread safety, and it would seem useful to show it in Javadoc. Javadoc, however, does not print the synchronized keyword in method declarations. Synchronization is considered an implementation detail, not a contract to expose in the API documentation. Item 82 of Effective Java, Third Edition, defends this policy on the same grounds.

synchronized on a method declaration is equivalent to wrapping the whole method body in a synchronized(this) block. That is, the following code

synchronized on the method declaration
synchronized void run() {
    // do something
}

does the same thing as this code:

The whole method body in a synchronized block
void run() {
    synchronized (this) {
        // do something
    }
}

As the implementation improves, the synchronized region may shrink to part of the method, or a separate lock object may replace this. Such implementation details change more often than the external interface. It is also hard to show every lock-protected region in the method declaration. Some classes synchronize internally with java.util.concurrent.locks.Lock or CAS (Compare And Swap) operations. The presence or absence of the synchronized keyword therefore cannot be the only criterion for thread safety.

The Need for a Class-Level Thread Safety Statement

In a multithreaded environment, each method call may be safe on its own while a combination of calls is not. For example, a HashMap that is safely published after construction (for instance, through a final field or a volatile variable) and never modified afterward can be read with get() from several threads without problems. But if one thread changes its structure with put() while another calls get(), the result is not guaranteed. The same applies to Hashtable, whose public methods are all synchronized or delegate to synchronized methods and views, and to a Map wrapped with Collections.synchronizedMap(). When two calls are combined, such as checking with containsKey() and then calling put(), a race condition occurs unless the caller holds a lock externally.

What needs documenting, then, is not whether methods are synchronized but under what conditions the class is safe. Since Javadoc has no convention for this, developers have to write it in the class description themselves. The Effective Java classification in the next section provides the criteria.

The Five Levels of Thread Safety in Effective Java

Item 82 of Effective Java, Third Edition, "Document thread safety" (Item 70 in the Second Edition), recommends documenting thread safety in five levels.

  • Immutable

    • The state never changes, so no external synchronization is needed.

    • Examples: String, Long, BigInteger (the book’s examples), java.time.LocalDate

  • Unconditionally thread-safe

    • The class has mutable state but synchronizes sufficiently inside.

    • Examples: AtomicLong, ConcurrentHashMap

  • Conditionally thread-safe

    • Some methods require external synchronization to be safe.

    • Example: a List wrapped with Collections.synchronizedList(). While iterating with an iterator, the caller must hold the List object as a lock. Otherwise, the behavior is non-deterministic. A fail-fast iterator may throw ConcurrentModificationException, but there is no guarantee that it will.

  • Not thread-safe

    • The caller must synchronize externally.

    • Examples: HashMap, ArrayList

  • Thread-hostile

    • The class cannot be used by multiple threads even with external synchronization. Classes or methods that modify static data without synchronization fall into this category. Even if each caller locks a different instance, they all end up modifying the same static data concurrently.

    • Examples: The Third Edition explains that the generateSerialNumber method in Item 78 would be thread-hostile if it incremented a static field without internal synchronization. System.runFinalizersOnExit(), the example in the Second Edition, was deprecated in JDK 1.2 and removed in JDK 11.

Examples in Class Descriptions

Here are three examples of where and how the Java standard library and Spring Batch describe thread safety.

java.util.LinkedList

The JDK 25 Javadoc for LinkedList states in bold, in the third paragraph of the class description, that it is "not synchronized".

JDK 25 Javadoc for LinkedList

java.text.SimpleDateFormat

The JDK 25 Javadoc for SimpleDateFormat has a final section of the class description titled "Synchronization", which says "Date formats are not synchronized". Its "API Note" recommends DateTimeFormatter as an "immutable and thread-safe alternative".

JDK 25 Javadoc for SimpleDateFormat

The JDK 25 Javadoc for DateTimeFormatter states "This class is immutable and thread-safe." under "Implementation Requirements" at the end of the class description.

JDK 25 Javadoc for DateTimeFormatter

The java.time package, added in JDK 8, applies this format across the whole package. In the JDK 25 source, 31 of the 43 public classes in java.time and its subpackages, including LocalDate, Instant, and ZonedDateTime, state the same sentence in the same place with the Javadoc @implSpec tag. All 12 public enums also use a sentence of the same shape, such as "This is an immutable and thread-safe enum." Of the remaining 12 classes, all but the utility class TemporalQueries state their immutability or threading conditions in the same section. Exception classes, for example, say "This class is intended for use in a single thread."

That said, this format has not become a standard across the whole JDK. @implSpec is not a standard Javadoc tag. It is a JDK-specific tag registered with the -tag option when the JDK is built. If you use it as is in an ordinary project, the JDK 25 javadoc command reports the error "unknown tag. Unregistered custom tag?". Within the JDK, new APIs also differ. Arena and Linker in java.lang.foreign, which became a final API in JDK 22, and ListFormat, added in JDK 22, state thread safety in @implSpec. In contrast, HexFormat, added in JDK 17, writes the same sentence "This class is immutable and thread-safe." in the body without @implSpec, and HttpClient, added in JDK 11, also says in the body "Once built, an HttpClient is immutable".

org.springframework.batch.item.file.FlatFileItemWriter

The FlatFileItemWriter Javadoc in Spring Batch 5.2.6 states on the last line of the class description "The implementation is not thread-safe.", with only "not" in bold.

Spring Batch 5.2.6 Javadoc for FlatFileItemWriter

As these examples show, each class marks thread safety in a different place and a different way. LinkedList and FlatFileItemWriter use bold text, and SimpleDateFormat uses a separate section heading. All four classes, however, put the statement in the middle or at the end of the class description rather than in the first paragraph, so anyone who does not read the API documentation to the end can easily miss it. I find myself wishing for a rule such as "Put thread safety on the first line of the class description, and always make it stand out."

Thread Safety with Annotations

Marking thread safety with annotations instead of prose gives it a consistent place and format in Javadoc. When an annotation’s declaration carries the @Documented meta-annotation, Javadoc prints that annotation as is in the class declaration. We first look at a project that defined its own annotation for this purpose, and then at annotations that many projects can share.

@Contract in Apache HttpClient

Apache HttpClient 5 defines a single annotation for thread safety, org.apache.hc.core5.annotation.Contract. Its threading attribute takes a value of the ThreadingBehavior enum.

ThreadingBehavior Meaning

IMMUTABLE

Fully immutable and thread-safe.

IMMUTABLE_CONDITIONAL

Immutable if the dependencies injected through the constructor are immutable, and thread-safe if they are thread-safe.

STATELESS

Stateless and thread-safe.

SAFE

Thread-safe.

SAFE_CONDITIONAL

Thread-safe only if the dependencies injected through the constructor are thread-safe.

UNSAFE

Not thread-safe. This is the default when the threading attribute is omitted.

In the HttpClient 5.5 source, the main classes are declared as follows.

HttpClient 5.5
@Contract(threading = ThreadingBehavior.SAFE)
public abstract class CloseableHttpClient implements HttpClient, ModalCloseable {

@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)
public class PoolingHttpClientConnectionManager
        implements HttpClientConnectionManager, ConnPoolControl<HttpRoute> {

@Contract(threading = ThreadingBehavior.SAFE)
public class BasicHttpClientConnectionManager implements HttpClientConnectionManager {

SAFE_CONDITIONAL has a name similar to "conditionally thread-safe" in Effective Java, but it means something different. The Effective Java classification is about which sequences of method calls need external synchronization, while the HttpClient value is about whether the dependencies received through the constructor are thread-safe.

@Contract carries the @Documented meta-annotation, so it appears in the class declaration in Javadoc. Below is the Javadoc for BasicHttpClientConnectionManager.

HttpClient 5 Javadoc for BasicHttpClientConnectionManager

Because it always appears in the same place, thread safety is visible at a glance. The class description also says "this class is fully thread-safe", but the annotation in the declaration catches the eye first.

HttpClient did not start with its own annotation. Up to HttpClient 4.5.2, HttpGet and the classes from the 5.5 example above were declared with @NotThreadSafe and @ThreadSafe, as shown below.

HttpClient 4.5.2
@NotThreadSafe
public class HttpGet extends HttpRequestBase {

@ThreadSafe
public abstract class CloseableHttpClient implements HttpClient, Closeable {

@ThreadSafe
public class PoolingHttpClientConnectionManager
    implements HttpClientConnectionManager, ConnPoolControl<HttpRoute>, Closeable {

@ThreadSafe
public class BasicHttpClientConnectionManager implements HttpClientConnectionManager, Closeable {

These annotations lived in the org.apache.http.annotation package, and their Javadoc said they originated in the book "Java Concurrency in Practice". Because of a licensing issue with the original JCIP library, discussed below, HttpCore 4.4.5 removed these four annotations, and HttpClient has used @Contract since 4.5.3.

JCIP Annotations

Each project can define its own annotations, as HttpClient did. Using annotations from widely adopted open source, however, spares users from learning something new. SpotBugs and IntelliJ IDEA, covered later, include the JCIP package in their default lists of recognized annotations. The JCIP annotations that HttpClient 4.x borrowed are the starting point for such shared annotations.

The JCIP annotations are the thread safety annotations proposed in Appendix A of "Java Concurrency in Practice". There are four:

  • @ThreadSafe: a thread-safe class

  • @NotThreadSafe: a class that is not thread-safe

  • @Immutable: an immutable class. An immutable class is thread-safe.

  • @GuardedBy("lock"): placed on a field or method to indicate which lock must be held when accessing it. It can name either an intrinsic lock used by synchronized or a java.util.concurrent.locks.Lock.

The first three annotations tell users about the class’s contract, and @GuardedBy tells maintainers of the class which lock to respect.

Effective Java, Second Edition (2008) came out after JCIP (2006) and explains its five levels by comparing them with the JCIP annotations. Lining up the two classifications, the four levels other than thread-hostile map onto the three class-level JCIP annotations. Unconditionally thread-safe and conditionally thread-safe are both covered by @ThreadSafe.

Effective Java level JCIP annotation Difference

Immutable

@Immutable

JCIP considers immutable classes thread-safe, so @ThreadSafe is not added on top.

Unconditionally thread-safe

@ThreadSafe

Same meaning.

Conditionally thread-safe

@ThreadSafe

JCIP has no separate name for this. Which lock to hold goes in the description. This is where the two classifications actually diverge.

Not thread-safe

@NotThreadSafe

JCIP treats this annotation as optional.

Thread-hostile

None

JCIP has no corresponding concept.

Not applicable

@GuardedBy("lock")

Placed on fields and methods, for maintainers. The five levels of Effective Java are a class-level contract with users, so the basis of classification itself is different.

In short, Effective Java divides classes into five levels by how much external synchronization users need. JCIP uses a safe-or-not dichotomy with immutability added as a special case, and separates information for maintainers into @GuardedBy. Section 4.5 of JCIP, "Documenting synchronization policies", recommends documenting thread safety guarantees for users and synchronization policies for maintainers.

Annotations Spread Under the JCIP Names

The original library distributed with the JCIP book is net.jcip:jcip-annotations:1.0 on Maven Central. Its license, however, is Creative Commons Attribution, a license that the Creative Commons foundation itself does not recommend for software. That led to com.github.stephenc.jcip:jcip-annotations:1.0-1, a reimplementation of the same API under the Apache License 2.0. The two libraries share the package name (net.jcip.annotations) and the annotation names.

The JCIP annotations have also been copied into several other projects. The following libraries use the same annotation names in different packages.

  • javax.annotation.concurrent: com.google.code.findbugs:jsr305, the JSR-305 annotation implementation distributed by FindBugs, contains the same four annotations. JSR-305 itself was abandoned without a final release and is dormant. On Java 9 and 10, the javax.annotation package in this jar placed on the module path could conflict with the JDK’s java.xml.ws.annotation module. That JDK module was removed in JDK 11, however, so the problem does not apply to every version since Java 9.

  • com.google.errorprone.annotations: @Immutable and @ThreadSafe used by Google’s Error Prone, plus @GuardedBy in the concurrent subpackage.

  • androidx.annotation.GuardedBy: for Android.

  • org.apache.http.annotation: as seen above, the four annotations Apache HttpComponents copied and used up to 4.5.2. They were replaced by @Contract in HttpCore 4.4.5 and HttpClient 4.5.3.

Choosing Annotations for Your Purpose

The original JCIP library is hard to recommend for new projects because of its license, and JSR-305 because its standardization was abandoned. Instead, considering the coverage of the static analysis tools in the next section, I recommend one of the following two libraries.

  • If compile-time verification comes first, error_prone_annotations is the right fit. With Error Prone, @Immutable and @GuardedBy can be checked at compile time. Guava declares this library as a compile-scope dependency (as of 33.7.1-jre), so projects that use Guava get it on the compile classpath as a transitive dependency. If you use it directly in code, though, it is safer to declare the dependency explicitly rather than rely on the transitive one. Error Prone does not check @ThreadSafe, and the library has no @NotThreadSafe. You therefore also need a convention that unmarked classes are considered not thread-safe.

  • If expressing all four JCIP annotations in your public API, including @NotThreadSafe, comes first, com.github.stephenc.jcip:jcip-annotations is the right fit. IntelliJ and SpotBugs include this package (net.jcip.annotations) in their default lists, so a project that uses both tools only needs to add one dependency.

What Static Analysis Tools Check

Annotations are valuable as documentation alone, but they become more useful when tools catch violations. This section describes how far SpotBugs, Error Prone, and IntelliJ IDEA each go. Tool coverage can change between versions, so here are the versions I ran or checked against documentation for this article.

Table 1. Verification baseline
Target Version and how it was checked

Java

JDK 25

SpotBugs

SpotBugs 4.10.4 with Gradle plugin 6.5.11, run on the examples

Error Prone

Error Prone 2.50.0 with Gradle plugin 5.1.1, run on the examples

IntelliJ IDEA

Inspectopedia 2026.2 documentation and the inspection registrations in IntelliJ Community source 826413b22cfe

SonarQube

Built-in rules of the Java analyzer in SonarJava source 9bf31b6e0037 and its list of SpotBugs external rules

Eclipse JDT

JDT Core Options and Eclipse JDT source 8c40c7d2ae12

In the full list of JDT Core Options as of September 2026 and in Eclipse JDT source 8c40c7d2ae12, I found no compiler check that deals with thread safety annotations. Eclipse users can fill the gap with the SpotBugs Eclipse plugin.

At the same point in time, I searched the built-in rule implementations in SonarJava source 9bf31b6e0037, the Java analyzer for SonarQube, for ThreadSafe, GuardedBy, and javax.annotation.concurrent. S3077 was the only implementation that referred to thread safety annotations. I found no rule that directly verifies contracts declared with annotations. The S3077 implementation, which flags volatile on reference-type fields, makes an exception when the field’s type carries @Immutable or @ThreadSafe from the JSR-305 package. It does not handle @GuardedBy.

SonarQube can import SpotBugs reports as external issues. Its list of external rules includes all three bug patterns covered below. To see these results in SonarQube, then, you run SpotBugs in the build to produce an XML report and pass it with sonar.java.spotbugs.reportPaths.

The SpotBugs and Error Prone examples are in examples/thread-safety-static-analysis.

SpotBugs

SpotBugs, the successor to FindBugs, has one bug pattern for the JCIP annotations, and it is for @Immutable. @GuardedBy and @ThreadSafe have no dedicated checks. Instead, a detector that looks for inconsistently synchronized fields reads them as input to its judgment. SpotBugs recognizes three packages:

  • net.jcip.annotations (original JCIP)

  • javax.annotation.concurrent (JSR-305)

  • jakarta.annotation.concurrent (recognized in advance as part of SpotBugs' support for the jakarta namespace; Jakarta Annotations 3.0.0 has no such package)

The JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS bug pattern, which dates back to FindBugs 2.0, warns when a class annotated with @Immutable has a field that is not final. The SpotBugs 4.10.4 implementation, however, excludes transient and volatile fields from this check.

Create a class declared @Immutable that nevertheless has a setter,

package net.benelog;

import net.jcip.annotations.Immutable;

/**
 * Declared {@code @Immutable} but has a non-final field, so
 * SpotBugs reports JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS.
 */
@Immutable
public class Memo {
	private String content;

	public void setContent(String content) {
		this.content = content;
	}

	public String getContent() {
		return content;
	}
}

configure the SpotBugs plugin in Gradle,

plugins {
	java
	id("com.github.spotbugs") version "6.5.11"
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("com.github.stephenc.jcip:jcip-annotations:1.0-1")
}

java {
	toolchain {
		languageVersion.set(JavaLanguageVersion.of(25))
	}
}

spotbugs {
	toolVersion.set("4.10.4")
	ignoreFailures.set(true)
	if (project.hasProperty("reportLow")) {
		// Report low-priority warnings too. The default reports medium and above only.
		reportLevel.set(com.github.spotbugs.snom.Confidence.LOW)
	}
}

tasks.spotbugsMain {
	val report = layout.buildDirectory.file("reports/spotbugs/main.txt")
	reports.create("text") {
		required.set(true)
		outputLocation.set(report)
	}
	doLast {
		println(report.get().asFile.readText())
	}
}

and run ./gradlew :spotbugs:spotbugsMain. The report contains this line:

M B JCIP: Memo.content should be final since net.benelog.Memo is marked as Immutable.  In Memo.java

The example sets ignoreFailures to true so that the report is printed in full even when there are warnings. To fail the build on violations in real CI, change this setting to false or omit it.

@GuardedBy is handled by the IS_FIELD_NOT_GUARDED bug pattern. This pattern, however, does not find violations by looking at the annotation. The IS2_INCONSISTENT_SYNC detector estimates missing synchronization from the proportion of field accesses made while holding a lock. When a field carries @GuardedBy("this"), the detector treats it differently in three ways:

  • It renames the bug pattern to IS_FIELD_NOT_GUARDED.

  • It keeps fields as candidates even if no access holds the lock.

  • It raises the warning priority.

The only value it recognizes is "this". The detector treats fields where less than half of the accesses hold the lock as likely false positives and lowers their priority.

That is why Counter in the same project is not reported with the default settings, even though it modifies a @GuardedBy("this") field without a lock. The read and write in increment() happen without a lock, and only the read in the synchronized method get() holds it. One of three accesses, or 33%, holds the lock. Only when you make the example report low-priority warnings too with ./gradlew :spotbugs:spotbugsMain -PreportLow does it appear.

L M IS: Counter.count not guarded against concurrent access; locked 33% of time  Unsynchronized access at Counter.java:[line 16]

The class below has the same violation plus two more synchronized methods, which brings the locked accesses to 60%.

package net.benelog;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

/**
 * Same violation as Counter, but more synchronized methods raise
 * the proportion of locked accesses. SpotBugs reports IS_FIELD_NOT_GUARDED.
 */
@ThreadSafe
public class LockedCounter {
	@GuardedBy("this")
	private int count;

	public void increment() {
		count++;
	}

	public synchronized int get() {
		return count;
	}

	public synchronized void reset() {
		count = 0;
	}

	public synchronized boolean isZero() {
		return count == 0;
	}
}

This class is reported with high priority even with the default settings. The first letter of the output is the priority and the second is the category.

H M IS: LockedCounter.count not guarded against concurrent access; locked 60% of time  Unsynchronized access at LockedCounter.java:[line 16]

The same detector also reads @ThreadSafe and @NotThreadSafe. It excludes fields of @NotThreadSafe classes from the check and uses @ThreadSafe on a class as grounds for raising the warning priority of its fields. It does not verify that the contract declared by the annotation is kept. A class declared @ThreadSafe can hold a field of a @NotThreadSafe type without a warning.

In short, the only thing SpotBugs checks deterministically from annotations alone is the final rule for @Immutable classes. @GuardedBy violations are also reported as estimates based on the proportion of locked accesses; the annotation only changes the candidates and priority of that estimate.

Error Prone

Google’s Error Prone plugs into javac and checks rules that prevent errors at compile time.

Checking @GuardedBy

Error Prone’s GuardedBy check reports a compile error when a field or method annotated with @GuardedBy(lock) is accessed without holding the specified lock. Unlike SpotBugs, whose reporting depends on access proportions, it decides for each access within its analysis scope whether the lock is held. It does not check accesses inside constructors and initializer blocks, or fields protected by a ReadWriteLock.

The annotations this check recognizes include:

  • com.google.errorprone.annotations.concurrent.GuardedBy

  • javax.annotation.concurrent.GuardedBy

  • androidx.annotation.GuardedBy for Android

The original JCIP net.jcip.annotations.GuardedBy is not on the list. The documentation states that the @Immutable check targets only com.google.errorprone.annotations.Immutable and excludes javax.annotation.concurrent.Immutable. The next section confirms this.

In Gradle, the net.ltgt.errorprone plugin attaches Error Prone to javac. To compare annotations from three packages, the example includes all three libraries.

plugins {
	java
	id("net.ltgt.errorprone") version "5.1.1"
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("com.github.stephenc.jcip:jcip-annotations:1.0-1")
	implementation("com.google.code.findbugs:jsr305:3.0.2")
	implementation("com.google.errorprone:error_prone_annotations:2.50.0")
	errorprone("com.google.errorprone:error_prone_core:2.50.0")
}

java {
	toolchain {
		languageVersion.set(JavaLanguageVersion.of(25))
	}
}

Compiling the following class, which uses the JSR-305 @GuardedBy,

package net.benelog;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

/**
 * JSR-305 package. Error Prone reports the @GuardedBy violation as a compile error.
 */
@ThreadSafe
public class JsrCounter {
	@GuardedBy("this")
	private int count;

	public void increment() {
		count++;
	}

	public synchronized int get() {
		return count;
	}
}

makes Error Prone 2.50.0 report this error:

JsrCounter.java:15: error: [GuardedBy] This access should be guarded by 'this', which is not currently held
		count++;
		^
    (see https://errorprone.info/bugpattern/GuardedBy)

JcipCounter, the same code with only the import changed to net.jcip.annotations.GuardedBy, compiles without any error. Conversely, ErrorProneCounter, which uses @GuardedBy from Error Prone’s own package, is caught with the same error as JsrCounter. The three classes are in the errorprone subproject of the example repository, and you can check them with ./gradlew :errorprone:compileJava.

Checking @Immutable

The Immutable check verifies that a class annotated with com.google.errorprone.annotations.Immutable is deeply immutable. The SpotBugs JCIP check only looks at whether fields are final, while Error Prone also checks whether the types of reference fields are immutable. The conservative definition of immutability in the Error Prone documentation also requires that this not escape from the constructor. The ImmutableChecker in 2.50.0, however, does not analyze this escaping from ordinary constructor bodies. The class below has one field that is not final and one field that is final but of a mutable type.

package net.benelog;

import java.util.List;

import com.google.errorprone.annotations.Immutable;

/**
 * Error Prone's own @Immutable. Both the non-final field and
 * the final field of a mutable type are reported as compile errors.
 */
@Immutable
public class ErrorProneMemo {
	private String content;
	private final List<String> tags;

	public ErrorProneMemo(String content, List<String> tags) {
		this.content = content;
		this.tags = tags;
	}

	public String getContent() {
		return content;
	}

	public List<String> getTags() {
		return tags;
	}
}

Both fields are reported as compile errors.

ErrorProneMemo.java:13: error: [Immutable] type annotated with @Immutable could not be proven immutable: 'ErrorProneMemo' has non-final field 'content'
	private String content;
	               ^
    (see https://errorprone.info/bugpattern/Immutable)
  Did you mean 'private final String content;'?
ErrorProneMemo.java:14: error: [Immutable] type annotated with @Immutable could not be proven immutable: 'ErrorProneMemo' has field 'tags' of type 'java.util.List<java.lang.String>', 'List' is mutable
	private final List<String> tags;
	                           ^
    (see https://errorprone.info/bugpattern/Immutable)

For tags to pass, its type must be one that Error Prone knows to be immutable or one annotated with @Immutable. A class that implements an interface annotated with @Immutable is subject to the same check. For generic classes, the containerOf attribute specifies which type parameters must be immutable.

JsrMemo, the same code with only the import changed to javax.annotation.concurrent.Immutable, compiles without errors. @GuardedBy is checked for the JSR-305 package too, but @Immutable is checked only for Error Prone’s own package.

Checking @ThreadSafe

com.google.errorprone.annotations.ThreadSafe also has a ThreadSafe check page. It sits in the "Experimental" group of the bug pattern list, so it looks as if you only need to turn it on, but in fact you cannot. Enabling it in the Gradle plugin with options.errorprone.error("ThreadSafe") fails the build with the error "ThreadSafe is not a valid checker name". BuiltInCheckerSuppliers, the list of built-in checks in the Error Prone 2.50.0 source, contains GuardedByChecker and ImmutableChecker but not ThreadSafeChecker. It was also missing from the earlier versions I checked, 2.3.0 through 2.45.0. The checker class itself is included in the error_prone_core jar.

So the class below compiles without any error under the default settings.

package net.benelog;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.google.errorprone.annotations.ThreadSafe;

/**
 * Error Prone's own @ThreadSafe. Error Prone 2.50.0 does not check it by default.
 * Registering ThreadSafeChecker with -PthreadSafeCheck reports fields modified without a lock
 * and final fields of non-thread-safe types as compile errors.
 */
@ThreadSafe
public class ErrorProneRegistry {
	private int count;
	private final Map<String, String> entries = new HashMap<>();
	private final ConcurrentHashMap<String, String> safeEntries = new ConcurrentHashMap<>();

	public void register(String key, String value) {
		count++;
		entries.put(key, value);
		safeEntries.put(key, value);
	}

	public int getCount() {
		return count;
	}
}

To see what this checker looks at, I created the threadsafe-check subproject in the example repository. It is experimental code that registers a subclass of ThreadSafeChecker in META-INF/services so that Error Prone loads it as a plugin check. Error Prone uses ServiceLoader to find checks on the compiler’s annotation processor path, with com.google.errorprone.bugpatterns.BugChecker as the service interface. So all you need is a file with that name that lists the checker class name.

com.google.errorprone.bugpatterns.threadsafety.ThreadSafeCheck

The registered class is a wrapper that extends ThreadSafeChecker and names the check with @BugPattern. Because the constructor of ThreadSafeChecker is package-private, the wrapper uses the same package name. It also has a nominal public no-argument constructor, which ServiceLoader requires. Since this code depends on the internals of 2.50.0, I do not recommend it for real projects.

package com.google.errorprone.bugpatterns.threadsafety;

@BugPattern(
		name = "ThreadSafe",
		summary = "Type declaration annotated with @ThreadSafe is not thread safe",
		severity = ERROR)
public class ThreadSafeCheck extends ThreadSafeChecker {

	/** Public no-argument constructor required by ServiceLoader. Error Prone uses the @Inject constructor. */
	public ThreadSafeCheck() {
		super(null, null);
	}

	@Inject
	ThreadSafeCheck(WellKnownThreadSafety wellKnownThreadSafety,
			ThreadSafeAnalysis.Factory threadSafeAnalysisFactory) {
		super(wellKnownThreadSafety, threadSafeAnalysisFactory);
	}
}

Adding this subproject as a dependency in the errorprone configuration of the project under analysis registers it as a plugin check. The example adds it only when a Gradle property is present; this part was omitted from the build.gradle.kts quoted earlier.

dependencies {
	errorprone("com.google.errorprone:error_prone_core:2.50.0")
	if (project.hasProperty("threadSafeCheck")) {
		// Register ThreadSafeChecker, which is not in the default check list, as a plugin.
		errorprone(project(":threadsafe-check"))
	}
}

Running ./gradlew :errorprone:compileJava -PthreadSafeCheck reports the following:

ErrorProneRegistry.java:16: error: [ThreadSafe] @ThreadSafe class fields should be final or annotated with @GuardedBy. See https://errorprone.info/bugpattern/ThreadSafe for details.
	private int count;
	            ^
    (see https://errorprone.info/bugpattern/ThreadSafe)
  Did you mean 'private final int count;'?
ErrorProneRegistry.java:17: error: [ThreadSafe] @ThreadSafe class has non-thread-safe field, 'Map' is not thread-safe
	private final Map<String, String> entries = new HashMap<>();
	                                  ^
    (see https://errorprone.info/bugpattern/ThreadSafe)

To pass this check, every instance field must be one of the following:

  • final, with a declared type that Error Prone knows to be thread-safe

  • annotated with @GuardedBy

static fields are not checked, and @LazyInit fields are exempt from the final requirement. count is caught because it is neither final nor @GuardedBy. entries is caught because its declared type Map is not a thread-safe type. Even if the actual object is a ConcurrentHashMap, a field declared as Map is caught. safeEntries, whose declared type is also ConcurrentHashMap, passes. ErrorProneCounter, shown earlier, passes this check because count has @GuardedBy("this"), and is caught only by the @GuardedBy check.

In short, as of September 2026, Error Prone checks @Immutable and @GuardedBy, but @ThreadSafe serves only as documentation unless you register the checker yourself as a plugin, as shown above.

IntelliJ IDEA

IntelliJ IDEA 2026.2 has six inspections in the Concurrency annotation issues group. Five are for @GuardedBy and one is for @Immutable. No inspection verifies contracts declared with @ThreadSafe.

Concurrency annotation issues inspections in IntelliJ IDEA 2026.2

The Unguarded field access or method call inspection recognizes @GuardedBy from all of these packages:

  • net.jcip.annotations

  • javax.annotation.concurrent

  • org.apache.http.annotation

  • com.android.annotations.concurrency

  • androidx.annotation

  • com.google.errorprone.annotations.concurrent

SpotBugs also reads the original JCIP package, but it handles only @GuardedBy("this") and stops at warnings guessed from access proportions. IntelliJ, by comparison, directly checks the guard expressions of the original JCIP annotation.

The Non-final field in @Immutable class inspection warns when an @Immutable class has a field that is not final. It does not check whether field types are mutable, so its depth is closer to SpotBugs than to Error Prone. Unlike SpotBugs, though, it also warns about transient and volatile fields. The markers this inspection recognizes are:

  • @Immutable in the original JCIP and JSR-305 packages

  • Error Prone’s com.google.errorprone.annotations.Immutable

  • AutoValue’s @AutoValue

  • The Javadoc @Immutable tag

@ThreadSafe is read only as a hint by the Access to static field locked on instance inspection in the "Threading issues" group. This inspection warns when code holding an instance lock accesses a non-constant static field. If the accessed field is final, it checks the annotations on the field’s declared type and skips the warning if a registered thread safety annotation is present. It therefore does not verify the contract of the annotated class itself. The default list contains these annotations:

  • @ThreadSafe in the original JCIP, JSR-305, org.apache.http.annotation, and com.android.annotations.concurrency packages

  • @AnyThread in the androidx.annotation and android.support.annotation packages

Error Prone’s @ThreadSafe is not in the default list.

However, the inspection registrations in IntelliJ IDEA 2026.2 declare every inspection in the "Concurrency annotation issues" group with enabledByDefault="false" and level="WARNING". You have to turn them on yourself under Settings | Editor | Inspections | Java | Concurrency annotation issues. Even when enabled, they are editor warnings and cannot block javac compilation the way Error Prone does. Enforcing them in CI requires a separate runner such as Qodana. Qodana is a code quality platform from JetBrains that runs the same inspections as IntelliJ IDEA in CI, as a Docker image or a command-line tool. You enable the checks in a Qodana profile and set a failure condition.

Across the three tools, the range checked deterministically from annotations alone is narrow.

  • SpotBugs directly checks only the final rule for @Immutable classes. It guesses @GuardedBy violations from the proportion of locked accesses, and reads @ThreadSafe and @NotThreadSafe only as input to that guess.

  • Error Prone catches missing locks for @GuardedBy and deep immutability for @Immutable as compile errors. It does not recognize the original JCIP package, however, and the @ThreadSafe check works only if you register it separately.

  • IntelliJ IDEA directly checks @GuardedBy from many packages, including the original JCIP, and checks @Immutable at about the same depth as SpotBugs. Its inspections are off by default, though, and are not tied to javac compilation. Enforcing them requires a separate runner such as Qodana or a command-line inspection.

This is why I recommended either error_prone_annotations or jcip-annotations earlier. If you need checks that block compilation, use annotations that Error Prone recognizes. If you want to mark all four annotations and still get checks from your IDE and SpotBugs, use the net.jcip.annotations package (the Apache-licensed reimplementation).

Project-Specific Rules Verified with ArchUnit

The static analysis tools above partially check whether a class carrying an annotation is implemented as declared. Application code, however, has another kind of accident: an object shared by multiple threads holds, as a field and without synchronization, a class marked as not thread-safe.

For example, Spring @RestController and @Service beans are singletons unless a separate scope is specified, so all request threads share their fields. You could use another scope such as request scope. Some projects, however, prevent the problem by making it a policy that controllers must not hold such types as fields at all. Rules like this depend on the structure each project aims for, so general-purpose static analysis tools do not have them. With ArchUnit, you can write such a rule as a JUnit test and check it on every build.

Example Code

The example has a class annotated with @NotThreadSafe and a controller that holds it as a field. The controller also has a SimpleDateFormat field. The full project is in examples/thread-safety-archunit.

package net.benelog.report;

import net.jcip.annotations.NotThreadSafe;

@NotThreadSafe
public class ReportFormatter {
	private final StringBuilder buffer = new StringBuilder();

	public String format(String title, String body) {
		buffer.setLength(0);
		return buffer.append(title).append('\n').append(body).toString();
	}
}
package net.benelog.web;

import java.text.SimpleDateFormat;
import java.util.Date;

import net.benelog.report.ReportFormatter;
import net.benelog.report.ReportService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ReportController {
	private final ReportService reportService;
	private final ReportFormatter formatter = new ReportFormatter();
	private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

	public ReportController(ReportService reportService) {
		this.reportService = reportService;
	}

	@GetMapping("/reports/{id}")
	public String report(@PathVariable long id) {
		return formatter.format(dateFormat.format(new Date()), reportService.find(id));
	}
}

Rules and Results

The ArchUnit test consists of two rules.

The first rule fails if the type of any field in a class annotated with @RestController carries @NotThreadSafe, without separately analyzing external synchronization or bean scope. ArchUnit reads annotations from bytecode. It can therefore check annotations with RUNTIME retention, such as the original JCIP annotations, as well as those with CLASS retention, such as the JSR-305 annotations and HttpClient’s @Contract. The example rule looks only at net.jcip.annotations.NotThreadSafe, though, so checking annotations from other packages or the threading value of @Contract requires additional conditions. Library classes outside the analyzed packages are also read from the classpath under the default settings, so the same rule can check @NotThreadSafe that a library has applied.

The second rule is for JDK classes that carry no thread safety annotation, such as SimpleDateFormat. It lists Format, Calendar, and StringBuilder explicitly.

package net.benelog;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;

import java.text.Format;
import java.util.Calendar;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import net.jcip.annotations.NotThreadSafe;
import org.springframework.web.bind.annotation.RestController;

@AnalyzeClasses(packages = "net.benelog")
class ThreadSafetyArchTest {

	@ArchTest
	static final ArchRule controllers_should_not_hold_not_thread_safe_types =
			fields().that().areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
					.should().notHaveRawType(annotatedWith(NotThreadSafe.class))
					.because("controllers are singletons by default, so all request threads share their fields");

	private static final DescribedPredicate<JavaClass> KNOWN_NOT_THREAD_SAFE_JDK_TYPES =
			assignableTo(Format.class)
					.or(assignableTo(Calendar.class))
					.or(assignableTo(StringBuilder.class))
					.as("JDK types that are not thread-safe (Format, Calendar, StringBuilder)");

	@ArchTest
	static final ArchRule controllers_should_not_hold_known_not_thread_safe_jdk_types =
			fields().that().areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
					.should().notHaveRawType(KNOWN_NOT_THREAD_SAFE_JDK_TYPES)
					.because("JDK classes carry no thread safety annotations, so a list blocks them");
}

Running ./gradlew test with ArchUnit 1.5.0, Spring Web 7.0.9, JUnit 6.1.3, and JDK 25 fails both rules and reports which field broke each rule.

Architecture Violation [Priority: MEDIUM] - Rule 'fields that are declared in classes that are annotated with @RestController should not have raw type annotated with @NotThreadSafe, because controllers are singletons by default, so all request threads share their fields' was violated (1 times):
Field <net.benelog.web.ReportController.formatter> has raw type annotated with @NotThreadSafe in (ReportController.java:0)

Architecture Violation [Priority: MEDIUM] - Rule 'fields that are declared in classes that are annotated with @RestController should not have raw type JDK types that are not thread-safe (Format, Calendar, StringBuilder), because JDK classes carry no thread safety annotations, so a list blocks them' was violated (1 times):
Field <net.benelog.web.ReportController.dateFormat> has raw type JDK types that are not thread-safe (Format, Calendar, StringBuilder) in (ReportController.java:0)

HealthController in the same project, which holds only Clock and DateTimeFormatter as fields, passed both rules.

ReportController, caught by the rules, can be fixed by holding the immutable DateTimeFormatter as a field instead of SimpleDateFormat, and by confining ReportFormatter to a local variable of the request-handling method.

ReportController after the fix
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

@GetMapping("/reports/{id}")
public String report(@PathVariable long id) {
	ReportFormatter formatter = new ReportFormatter();
	return formatter.format(DATE_FORMAT.format(LocalDate.now()), reportService.find(id));
}

After changing ReportController this way and running the test again, both rules pass. ReportFormatter is created anew on each call and is not shared with other request threads. Passing the rules, however, does not prove the thread safety of the whole controller. Other shared state and race conditions in sequences of method calls need separate review.

This approach also has limits.

  • In this rule, ArchUnit looks only at the raw declared type of a field. It cannot catch an ArrayList assigned to a field declared as List, or a non-thread-safe type used as a generic type argument.

  • It also misses cases where only a supertype carries the annotation and the actual declared type does not, unless you traverse the hierarchy separately.

  • The rule does not judge whether field access is protected by an external lock or whether the controller has a separate scope.

  • JDK types without annotations require a manually maintained list, as in the second rule.

  • The rule looks only at fields declared directly in classes annotated with @RestController. It therefore also misses fields inherited from an unannotated superclass.

Summary

When a Java class is used by multiple threads in a way it was not designed for, the side effects are serious and the source of the problem is hard to trace. That is why thread safety must be documented clearly. Writing it in Javadoc prose, however, leaves its place and wording different in every class, and it works only if people read carefully.

Marking thread safety with annotations gives it a consistent place and format and makes it readable by tools. I recommend Error Prone’s annotations or the Apache-licensed reimplementation of the JCIP annotations. Error Prone blocks @GuardedBy and @Immutable violations with compile errors, IntelliJ IDEA reports some violations as editor warnings, and SpotBugs reports some in its analysis reports. None of the three tools, however, verifies contracts declared with @ThreadSafe under default settings.

For thread safety rules specific to your project, I recommend checking them with ArchUnit.

References