wolfgang ziegler


„make stuff and blog about it“

Getter-Setter Pattern Considered Harmful

February 14, 2025

The Problem

Many developers (like myself) who started their careers with OOP (Java, C#, ...) during the last decades will look at this code and find nothing wrong about it at first sight.

public class Person {
  private String name;
  private int age;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

In fact, everything's wrong with this code fragment. However, we have become so used the obvious problem, that it can hide in plain sight.

The reason for that is, that we have been indoctrinated - almost brainwashed - with OOP and its getter-setter pattern to such an extent, that we kept ignoring the obvious problems for years: mutability and state.

Mutability and state are what make code complex and hard to reason about. When hunting a bug or reviewing a PR, this is what's usually hardest to wrap our heads around. Did a certain value change over time or does it still have its initial state? Who knows? Add multithreading and race conditions to the mix and we have a recipe for disaster.

So, when designing classes we should ask ourselves this simple question:

Can I make it immutable?

Immutability To The Rescue

So, let's apply that to the code fragment above.

public class Person {
  private final String name;
  private final int age;

  public Person(final String name, final int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public int getAge() {
    return age;
  }
}

Much better already! The member variables are immutable (final) now and we got rid of those useless setters.

But we can do even better.

public class Person {

  // ... omitted for brevity

  public String name() {
    return name;
  }

  public int age() {
    return age;
  }
}

Notice the subtle difference? Dropping those get prefixes for the access methods has significant benefits. Let's think about it: What value was added by the prefix? Correct, none!

The method names are already descriptive enough with the get prefix. And by getting rid of this prefix we moved any mental reference to the dreaded getter-setter pattern (let's call it an anti-pattern from now on) completely out of the picture.

Of course, sometimes we actually need to modify data over time. Our first approach in this case should always be to create an immutable copy of an object.

Fortunately, there's a helpful pattern for that.

The with Pattern

We provide with-prefixed methods for those members of the class that we want to able to modify. These methods return fresh immutable copies and leave the original instance without change.

public class Person {
  
  // ... omitted for brevity

  public Person withName(String newName) {
      return new Person(newName, this.age);
  }

  public Person withAge(int newAge) {
      return new Person(this.name, newAge);
  }
}

For more complex classes, the Builder pattern is a useful addition here.

It also helps initially with classes having optional members or where the constructor argument list would become unreasonably long.

The Builder Pattern

Here's the updated code with following features:

  • full immutability
  • the with pattern
  • the Builder pattern
public class Person {
  private final String name;
  private final int age;

  private Person(Builder builder) {
    this.name = builder.name;
    this.age = builder.age;
  }

  public String name() {
    return name;
  }

  public int age() {
    return age;
  }

  public Person withName(String newName) {
    return new Person(new Builder().name(newName).age(this.age));
  }

  public Person withAge(int newAge) {
    return new Person(new Builder().name(this.name).age(newAge));
  }

  public static Builder builder() {
    return new Builder();
  }

  public static class Builder {
    private String name;
    private int age;

    public Builder name(String name) {
      this.name = name;
      return this;
    }

    public Builder age(int age) {
      this.age = age;
      return this;
    }

    public Person build() {
      return new Person(this);
    }
  }
}

Actual Mutability

Last but not least, let's look at the situations, where we actually cannot avoid mutable code.

Here are my recommendations for these situations:

  • Make those mutable classes as small as possible.
    • Rip out the mutable parts of a class and encapsulate them in a smaller more concise one.
    • That way we keep the mutable aspects of our code contained and easier to reason about.
  • Also: "be honest!" about those data structures!
    • Drop the getters and setters. They don't add any value (unless they perform validation, which they never do)!

Here's an honest version of our mutable class above:

public class Person {
  public String name;
  public int age;
}

Drop The Baggage

As we see, we have been lied to. Adding getters and setters all over our classes, actually made our code worse. By erasing this anti-pattern from our brains, our code will become:

  • less error-prone
  • better suited for concurrency
  • simpler to read and reason about