I reviewed a pull request last Tuesday that genuinely made my eye twitch.
A newer dev on my team was trying to wire up a massive enterprise web service. They had all the heavy framework annotations perfectly memorized. The dependency injection was flawless. But the actual business logic inside the service? A 600-line god class full of nested for loops, mutated state, and tightly coupled concrete classes.
Well, that’s not entirely accurate — I should clarify that learning the shiny new frameworks is fun. But if your foundation is cracked, your fancy microservice is just a very fast way to generate garbage.
And let’s strip away the enterprise bloat for a minute. If you’re writing Java in 2026, there are a few core concepts you need to handle cleanly before you even think about touching a web framework. I’m talking about basic classes, interfaces, and streams.
Classes and Methods: Stop Using Primitives for Everything
I see this constantly. People build a class to represent a domain object and just throw String and double at every problem. Don’t do that.
Let’s say we’re processing financial transactions. A transaction isn’t just a pile of loose data. It’s a specific concept. Here is a clean, modern way to structure a standard Java class with proper methods.
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
public class Transaction {
private final String id;
private final BigDecimal amount;
private final String currency;
private final LocalDateTime timestamp;
private boolean isProcessed;
public Transaction(BigDecimal amount, String currency) {
// Fail fast. Don't let bad data live in your system.
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
this.id = UUID.randomUUID().toString();
this.amount = amount;
this.currency = currency;
this.timestamp = LocalDateTime.now();
this.isProcessed = false;
}
// Business method, not just a dumb setter
public void markAsProcessed() {
if (this.isProcessed) {
throw new IllegalStateException("Transaction " + id + " is already processed.");
}
this.isProcessed = true;
}
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
public boolean isProcessed() { return isProcessed; }
}
Interfaces: Decouple Your Logic
If you hardcode your dependencies, you are going to hate your life when the product manager changes their mind next sprint.
Interfaces define a contract. They tell the rest of your application “I don’t care how you do this, just give me a result.” Suppose we need to validate these transactions before processing them. We might want a fraud check, a balance check, or a geographic check.
public interface TransactionValidator {
/**
* Evaluates a transaction and returns true if valid.
*/
boolean isValid(Transaction transaction);
// Default methods are great for providing standard fallback behavior
default String getValidatorName() {
return this.getClass().getSimpleName();
}
}
Streams: The Gotcha Everyone Falls For
Java 8 dropped over a decade ago. There is almost zero excuse for writing manual for loops to filter and transform collections anymore. The Stream API is cleaner and less prone to off-by-one errors.
import java.util.List;
import java.util.stream.Collectors;
public class TransactionProcessor {
private final List<TransactionValidator> validators;
public TransactionProcessor(List<TransactionValidator> validators) {
this.validators = validators;
}
public List<BigDecimal> getValidUsdAmounts(List<Transaction> transactions) {
return transactions.stream()
.filter(t -> !t.isProcessed())
.filter(t -> "USD".equals(t.getCurrency()))
.filter(this::passesAllValidators)
.map(Transaction::getAmount)
.collect(Collectors.toList());
}
private boolean passesAllValidators(Transaction t) {
return validators.stream().allMatch(v -> v.isValid(t));
}
}
That reads like plain English. “Filter unprocessed, filter USD, filter valid, map to amount, collect to list.”
But here is the trap I see constantly: parallelStream().
Developers discover parallelStream() and immediately slap it onto every collection they have, assuming it’s a magic “make my code faster” button. I actually sat down and benchmarked this specific behavior on my M3 Mac running Java 21.0.2. And the .parallelStream() took 47 milliseconds, while the standard sequential .stream() processed the batch in about 14 milliseconds.
Stick to sequential streams until a profiler explicitly tells you otherwise.
Get the basics right first. The rest is just syntax.
