Large Java objects often end up with toString() methods containing dozens of conditional checks. This guide shows clean, performant, production-ready ways to refactor such methods without clutter, while keeping full control over formatting and avoiding reflection.
Starting Point: The Problem
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this, CUSTOM_STYLE);
if (StringUtils.isNotBlank(requestId)) {
builder.append("REQUEST_ID", requestId);
}
if (StringUtils.isNotBlank(userId)) {
builder.append("USER_ID", userId);
}
if (StringUtils.isNotBlank(status)) {
builder.append("STATUS", status);
}
if (StringUtils.isNotBlank(total)) {
builder.append("TOTAL", total);
}
// ... many more if statements
return builder.toString();
}
Problems:
- High noise-to-signal ratio
- Repetitive conditionals
- Hard to extend
- Easy to break formatting
Step 1: Extract the Conditional Logic (Baseline Refactor)
Helper Method
private static void appendIfNotBlank(
ToStringBuilder builder,
String key,
String value
) {
if (StringUtils.isNotBlank(value)) {
builder.append(key, value);
}
}
Refactored toString()
@Override
public String toString() {
ToStringBuilder builder =
new ToStringBuilder(this, CUSTOM_STYLE);
appendIfNotBlank(builder, "REQUEST_ID", requestId);
appendIfNotBlank(builder, "USER_ID", userId);
appendIfNotBlank(builder, "REQUEST_TYPE", requestType);
appendIfNotBlank(builder, "STATUS", status);
appendIfNotBlank(builder, "TOTAL", total);
appendIfNotBlank(builder, "MESSAGE_CODE", messageCode);
appendIfNotBlank(builder, "REQUEST_INITIATOR", requestInitiator);
appendIfNotBlank(builder, "CONSENT_ID", consentId);
appendIfNotBlank(builder, "NUMBER_OF_CONSENTS", numberOfConsents);
appendIfNotBlank(builder, "FI_ID", externalFI);
if (externalFiStatus != null) {
builder.append("EXTERNAL_FI_STATUS", externalFiStatus);
}
appendIfNotBlank(builder, "EXTERNAL_FI_MESSAGE_CODE", externalFiMessageCode);
appendIfNotBlank(builder, "EXTERNAL_FI_TOTAL_TIME", externalFITotal);
return builder.toString();
}
Characteristics
- ✔ Same runtime cost as raw
if - ✔ No allocations
- ✔ No streams
- ✔ No reflection
- ✔ Extremely readable
➡️ This is the recommended default for production systems
Step 2: Consumer-Based Pattern (Functional, Optional)
If you want to reduce visual repetition even further, use Consumer<ToStringBuilder>.
Consumer Factory
private static Consumer<ToStringBuilder> appendIfNotBlank(
String key,
String value
) {
return builder -> {
if (StringUtils.isNotBlank(value)) {
builder.append(key, value);
}
};
}
Usage
@Override
public String toString() {
ToStringBuilder builder =
new ToStringBuilder(this, CUSTOM_STYLE);
Stream.of(
appendIfNotBlank("REQUEST_ID", requestId),
appendIfNotBlank("USER_ID", userId),
appendIfNotBlank("REQUEST_TYPE", requestType),
appendIfNotBlank("STATUS", status),
appendIfNotBlank("TOTAL", total),
appendIfNotBlank("MESSAGE_CODE", messageCode)
).forEach(c -> c.accept(builder));
if (externalFiStatus != null) {
builder.append("EXTERNAL_FI_STATUS", externalFiStatus);
}
return builder.toString();
}
Trade-offs
| Aspect | Result |
|---|---|
| Readability | High |
| Performance | Slightly lower |
| Allocations | Lambda + Stream |
| Best use | Configurable / declarative code |
Step 3: Zero-Lambda, Zero-Stream Loop (Highly Performant)
If this method is used inside hot paths (logging, metrics), this pattern avoids lambdas entirely.
Field Descriptor
record Field(String key, String value) {}
Implementation
@Override
public String toString() {
ToStringBuilder builder =
new ToStringBuilder(this, CUSTOM_STYLE);
Field[] fields = {
new Field("REQUEST_ID", requestId),
new Field("USER_ID", userId),
new Field("REQUEST_TYPE", requestType),
new Field("STATUS", status),
new Field("TOTAL", total),
new Field("MESSAGE_CODE", messageCode)
};
for (Field field : fields) {
if (StringUtils.isNotBlank(field.value())) {
builder.append(field.key(), field.value());
}
}
if (externalFiStatus != null) {
builder.append("EXTERNAL_FI_STATUS", externalFiStatus);
}
return builder.toString();
}
Why This Is Fast
- No streams
- No lambdas
- Predictable allocations
- Easy to reorder fields
❌ Patterns to Avoid
❌ Reflection-Based toString()
for (Field f : getClass().getDeclaredFields()) {
...
}
Why avoid:
- Slow
- Breaks encapsulation
- Hard to format
- Dangerous for logs
❌ Map-Driven Approach
Map<String, String> values = new HashMap<>();
Why avoid:
- Unnecessary allocations
- Worse performance
- Less explicit
- Harder debugging
Final Recommendation
Use This in 90% of Cases
appendIfNotBlank(builder, "FIELD", value);
✔ Fast
✔ Explicit
✔ Readable
✔ Testable
✔ Production-safe
Summary
| Approach | Recommended |
|---|---|
| Helper method | ✅ Yes |
| Consumer + Stream | ⚠️ Optional |
| Loop over descriptors | ✅ Yes |
| Reflection | ❌ No |
| Map-based | ❌ No |


