Optimizing toString() Implementations in Java: Reflection vs. Manual Builders

When working with Java domain models or data-transfer objects, the toString() method becomes essential for logging, debugging, and monitoring application behavior. Many teams rely on libraries such as Apache Commons Lang’s ToStringBuilder for structured output, but as classes grow in size, the traditional approach—manually appending each field—quickly becomes repetitive and difficult to maintain.

In this article, we explore a more efficient approach using Java Reflection, highlight its advantages and drawbacks, and compare alternative solutions that may be better suited for modern applications.


Traditional Approach: Manual Field Appending

A classic implementation looks like this:

@Override
public String toString() {
    ToStringBuilder builder = new ToStringBuilder(this, MyCustomStyle.INSTANCE);

    if (StringUtils.isNotBlank(requestId)) {
        builder.append("REQUEST_ID", requestId);
    }
    if (StringUtils.isNotBlank(userId)) {
        builder.append("USER_ID", userId);
    }
    // … dozens of similar lines …

    return builder.toString();
}

Drawbacks

  • Repetitive and error-prone.
  • Hard to maintain: adding or renaming fields requires updating the method manually.
  • Large classes generate long, unreadable code blocks.
  • Encourages duplication across multiple domain classes.

A Better Solution: Using Reflection in toString()

Reflection allows you to iterate through all fields dynamically, eliminating the need for repetitive code. Here’s a cleaner, more maintainable implementation:

@Override
public String toString() {
    ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.JSON_STYLE);

    Field[] fields = getClass().getDeclaredFields();
    for (Field field : fields) {
        try {
            field.setAccessible(true);
            Object value = field.get(this);
            if (value != null && !value.toString().isBlank()) {
                builder.append(field.getName(), value);
            }
        } catch (IllegalAccessException ignored) {}
    }

    return builder.toString();
}

Pros of Using Reflection

1. Significantly Less Boilerplate

You eliminate dozens of if (value != null) blocks.

2. Automatically Includes New Fields

Any new variable added to the class appears in the log without modifying the method.

3. Cleaner and More Readable Code

Your class focuses on business logic, not logging maintenance.

4. Flexible Output Formats

Combined with ToStringStyle, results can be JSON-like, multi-line, or custom.


Cons of Using Reflection

1. Performance Overhead

Reflection is slower than direct field access.
However, for logging and debugging, the performance cost is usually negligible.

2. Sensitive Data Exposure

Reflection dumps all fields unless filtered, which may accidentally log:

  • passwords
  • tokens
  • internal IDs

You should explicitly exclude sensitive fields using annotations or naming conventions.

3. Limited Compile-Time Safety

If a field should not be logged, you must enforce this manually.

4. Issues in Large Object Graphs

Recursive objects may cause circular references unless guarded.


Safer Alternatives

Below are common alternatives depending on your architecture, performance needs, and security constraints.


1. Lombok @ToString (Most Popular Option)

@ToString(onlyExplicitlyIncluded = true)
public class MyModel {
    @ToString.Include
    private String requestId;

    @ToString.Include
    private String status;
}

Pros

  • Compile-time generation, zero runtime overhead.
  • Exclude fields using @ToString.Exclude.
  • Clean and maintainable.

Cons

  • Requires Lombok dependency and annotation processing.
  • Some teams avoid Lombok for portability reasons.

2. Jackson or Gson Serialization

Serialize an object to JSON for structured logging:

new ObjectMapper().writeValueAsString(this);

Pros

  • Produces clean JSON logs.
  • Allows inclusion/exclusion via annotations (@JsonIgnore, @JsonProperty).
  • Widely used in microservices and observability pipelines.

Cons

  • Slightly heavier than manual implementations.
  • Requires configuring mappers and modules.

3. Apache Commons ReflectionToStringBuilder

Apache Commons already offers reflection:

ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE);

Pros

  • Small change from existing code.
  • Very compact and straightforward.

Cons

  • Harder to customize field inclusion.
  • Performance still slower than manual code.

4. Manually Curated Logging DTO

For high-sensitivity apps (finance, identity, compliance), the safest option is a specific logging DTO:

public LogRecord toLog() {
    return new LogRecord(requestId, userId, status);
}

Pros

  • Zero risk of leaking confidential fields.
  • Clear control over what is logged.

Cons

  • Requires explicit maintenance of the DTO.

Which Approach Should You Use?

Use Reflection if:

  • You have many DTOs with many fields.
  • Performance is not critical for this operation.
  • You want minimal maintenance overhead.

Use Lombok if:

  • You want compile-time safety and clean code.
  • Your team already uses Lombok.

Use JSON Serialization if:

  • You want reliably structured logs.
  • Your observability tools parse JSON.

Use Manual DTOs if:

  • You work with personal data, banking information, or sensitive identifiers.
This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back