When writing complex projects, it’s essential to develop a good code culture. Usage of immutable and consistent objects is one of the most important ones. You can neglect this and write complex objects as standard ones, but it will be a significant source of bugs when the project grows enough. In my previous , I’ve shown how we can improve the consistency and reliability of standard objects. In a few words: article Add validations when setting values Use for each nullable field java.util.Optional Place complex mutations in a proper place - to the responsible class itself. But those actions are not enough to make fully reliable objects. In this article, I’ll show how to make objects immutable and make them eloquently and efficiently. The Problem If we make a simple serializable object with default constructor/getters/setters, it’s OK to make it the standard way. But let’s assume we write something more complex. And most likely, it’s used throughout countless places. For example, it’s used in HashMaps and maybe in a multi-threading environment. So, it doesn’t seem like a good idea anymore - to write it the default way. is not a big deal, and an inconsistent state between threads won’t make wait long. Breaking HashMap The first that come to mind when thinking about making immutable objects are: things Do not make setter methods Make all fields final Don’t share instances to mutable objects Do not allow subclasses to override methods (I will omit it in this article) But how to live with that kind of object? When we need to change it, we need to make a copy; how can we do it well without copy-pasting code and logic every time? A few words about our example classes Let’s say we have Accounts. Each account has an , , and . Accounts can be verified via email. When the status is , we do not expect the email to be filled. But when it is or , the email must be filled. id status email CREATED VERIFIED INACTIVE public enum AccountStatus { CREATED, VERIFIED, INACTIVE } The canon implementation: Account.java public class Account { private final String id; private final AccountStatus status; private final String email; public Account(String id, AccountStatus status, String email) { this.id = id; this.status = status; this.email = email; } // equals / hashCode / getters } Let’s imagine we’ve created an account. Then, somewhere in business logic, we need to change an email. var account = new Account("some-id", CREATED, null); How can we do that? The default way won’t work, we can’t have setters with an immutable class. account.setEmail("example@example.com");// we can not do that, we have no setters The only way to do that is to create a new instance and place to constructor previous values: var withEmail = new Account(account.getId(), CREATED, "example@example.com"); But it’s not the best way to change a field’s value, it produces so much copy/paste, and the Account class isn’t responsible for its consistency. Solution The suggested solution is to provide mutation methods from the Account class and implement copying logic inside the responsible class. Also, it’s essential to add required validations and usage of Optional for the email field, so we won’t have NPE or consistency problems. To build an object, I use the ‘Builder’ pattern. It’s pretty famous, and there are plenty of plugins for your IDE to generate it automatically. public class Account { private final String id; private final AccountStatus status; private final Optional<String> email; public Account(Builder builder) { this.id = notEmpty(builder.id); this.status = notNull(builder.status); this.email = checkEmail(builder.email); } public Account verify(String email) { return copy() .status(VERIFIED) .email(of(email)) .build(); } public Account changeEmail(String email) { return copy() .email(of(email)) .build(); } public Account deactivate() { return copy() .status(INACTIVE) .build(); } private Optional<String> checkEmail(Optional<String> email) { isTrue( notNull(email).map(StringUtils::isNotBlank).orElse(false) || this.status.equals(CREATED), "Email must be filled when status %s", this.status ); return email; } public static final class Builder { private String id; private AccountStatus status; private Optional<String> email = empty(); private Builder() { } public static Builder account() { return new Builder(); } public Builder id(String id) { this.id = id; return this; } public Builder status(AccountStatus status) { this.status = status; return this; } public Builder email(Optional<String> email) { this.email = email; return this; } public Account build() { return new Account(this); } } // equals / hashCode / getters } As you can see, there is a private method in our class that returns with an exact copy. That removes copy-pasting of all fields, however, it’s crucial that this method is not accessible from outside because, with that Builder outside, we lose control over the state and consistency. copy Builder Account.java Changing accounts in a new way Now, let’s create an account: var account = account() .id("example-id") .status(CREATED) .email((empty()) .build(); When we need to change an email, we don’t need to be responsible for creating a copy, we simply call a method from itself: Account var withNewEmail = account.changeEmail("new@new.com"); Demonstration of that in a unit test: @Test void should_successfully_change_email() { // given var account = account() .id("example-id") .status(VERIFIED) .email(of("old@old.com")) .build(); var newEmail = "new@new.com"; // when var withNewEmail = account.changeEmail(newEmail); // then assertThat(withNewEmail.getId()).isEqualTo(account.getId()); assertThat(withNewEmail.getStatus()).isEqualTo(account.getStatus()); assertThat(withNewEmail.getEmail()).isEqualTo(of(newEmail)); } To verify an account, we don’t create a copy with status and a new email. We simply call the method , which not only will it create a copy for us, but will also check the validity of an email. VERIFIED verify @Test void should_successfully_verify_account() { // given var created = account() .id("example-id") .status(CREATED) .build(); var email = "example@example.com"; // when var verified = created.verify(email); // then assertThat(verified.getId()).isEqualTo(created.getId()); assertThat(verified.getStatus()).isEqualTo(VERIFIED); assertThat(verified.getEmail().get()).isEqualTo(email); } Conclusion Living with immutable, consistent, and reliable objects is harsh, but in the proper way, it can become a lot easier. When implementing one, : don’t forget to Make all fields final Not provide setters Not share links to mutable objects Do not allow subclasses to override methods Provide mutation methods from your class Implement the method inside the responsible class that returns a , and use it to create new instances inside your class. private copy Builder Maintain consistency of fields by using validations on values Use with nullable fields Optional You can find the fully working example with more unit tests on . GitHub Lead Photo by on Adam Nieścioruk Unsplash