Every six months, like clockwork, a new JDK drops—and right on cue, the Java community’s ideological purity tests begin.

The usual suspects crawl out to complain about Project Lombok. They argue that it does “weird hacks” to the javac compiler, that it slows down upgrades, and that we don’t need it anymore because we have Records.

I am a little tired of this constant principled stance against its usage. I am an application developer, not a compiler engineer. I’ve been using Lombok for roughly 15 years now, and I can clearly say its benefits massively outweigh its cons. I’m glad I don’t work with people who hold that dogmatic opinion, because at the end of the day, we are paid to deliver working software—not to coddle the compiler.

I’m still betting on Lombok, and for me, the arguments against it usually fall flat in the real world.

Lombok introduces “weird bugs”

In over a decade and a half of using Lombok, I have only seen exactly one issue where Lombok was called out in a post-mortem as the root cause—and even then, the blame was unfair IMO.

We had a class annotated with @Data containing a primitive boolean field. When deserializing a message from SQS, the incoming payload was missing this field. Because it was defined as a primitive, Jackson assigned it the default value of false, which accidentally triggered a different code path.

The post-mortem labeled it a “Lombok issue.”

I disagree. Lombok simply generated the setter. The true root causes were:

  1. Bad data modeling: The field logically required three valid states (true, false, and null), so it should have been a boxed Boolean type from the start.
  2. Lack of test coverage: This edge case should have been caught in testing immediately, or flagged during the PR review.

Blaming Lombok for a developer coding error and a gap in test coverage is just lazy.

The Myth of “We Have Records Now”

Whenever you defend Lombok, someone inevitably chimes in with, “Just use Records!”

Records are fantastic for immutable data carriers, but they are not a complete replacement for Lombok. To prove this, I did a quick check on my current project to see which Lombok features we actually rely on.

Here is the raw data:

find . -name "*.java" | xargs cat | grep "import lombok" | sort | uniq -c | sort -n -r

    144 import lombok.extern.slf4j.Slf4j;
    131 import lombok.NonNull;
    120 import lombok.RequiredArgsConstructor;
     68 import lombok.Builder;
     30 import lombok.Data;
     24 import lombok.Getter;
     22 import lombok.NoArgsConstructor;
     22 import lombok.AllArgsConstructor;
     12 import lombok.experimental.Accessors;
      8 import lombok.extern.jackson.Jacksonized;
      6 import lombok.EqualsAndHashCode;
      4 import lombok.AccessLevel;
      2 import lombok.Singular;
      2 import lombok.experimental.UtilityClass;
      1 import lombok.ToString;
      1 import lombok.extern.log4j.Log4j2;

What dominates the top of that list is not @Data. It’s logging and code to support dependency injection.

I cannot imagine coding without @RequiredArgsConstructor. In Spring, you just declare your injected services as final fields, add that one annotation, and your constructor boilerplate vanishes. Records do absolutely nothing to solve this. Add @Slf4j and @Builder to the mix, and you are saving hundreds of lines of tedious, error-prone boilerplate per module that you’d otherwise have to write, review, and maintain.

And as for IDEs generating getters and setters? You still have to read that cluttered code. Plus, adding a new field means remembering to manually regenerate the equals and hashCode methods; a step that is easy to forget.

The Lombok “Dark Magic”

Purists argue that Lombok’s AST manipulation is “magic” and that modifying the compiler’s behavior is an architectural sin. But these same developers will happily build entire enterprise systems on top of frameworks that do far worse things at runtime.

Take Jackson, for example. The Java purist wants everything to be explicit and strictly abide by JVM rules. Yet, when Jackson deserializes a JSON payload into an object that lacks a default constructor, it often uses reflection (or libraries like Objenesis) to allocate memory for that object without ever actually calling a constructor. You are upset that Lombok generates a predictable setName() method at compile time, but you are perfectly fine with a framework completely bypassing the JVM’s object-creation lifecycle at runtime?

Or look at Hibernate and JPA. You define a standard List<Order> in your entity and write your business logic against it. But at runtime, Hibernate silently intercepts your object and replaces your standard Java ArrayList with its own PersistentBag proxy. If you try to iterate over that list after the database session closes, your application explodes with the dreaded LazyInitializationException.

That is more opaque, runtime sorcery where standard data structures are hijacked in an invisible manner.

Compared to that, Lombok is incredibly boring. The generated code exists completely at compile time. You can view the .class files. You can see exactly what it generated, and if a getter returns a value, you can step right through it. If we are willing to tolerate the massive, opaque runtime machinery of Jackson and Hibernate because of the velocity they provide, we should absolutely apply that exact same pragmatism to Lombok. AOP is another such tool with lots of gotchas - I remember once debugging a Try-Catch block in an AWS SWF (Simple Workflow) FLow Framework implementation where the Try and Catch blocks were literally executing on different machines.

A future JDK release will completely break Lombok’s compiler hacks

First, the upgrade panic is overblown. Unless you are raring to upgrade on day zero, Lombok is usually patched and ready within days of a major release. (I’m already running JDK 26 with Lombok).

But let’s say the absolute worst-case scenario happens, and Lombok is permanently banned from future JDKs. If that day comes, my migration strategy is incredibly simple:

  • Run delombok on my source code.
  • Commit the generated boilerplate and remove the dependency.
  • Upgrade the JDK in a separate PR.

There is zero runtime lock-in. Until that hypothetical doomsday arrives, I will gladly save my time and skip writing code that I simply don’t have to write. Purity is great in theory, but pragmatism ships features.