Enforces general code quality in Java 21+ — DRY, no magic numbers, immutability by default, sealed hierarchies and pattern matching over instanceof chains, encapsulation over getters and setters, and streams used where they clarify. Use when reviewing or refactoring Java for quality, and when the code shows duplication, hardcoded values, long if-else or switch chains on type, mutable shared state, anaemic classes, deep call chains like a.getB().getC().getD(), or clever one-liners. Also trigge...
Scanned 9/19/2026
npx -y skills add CasLubbers/code-design-skills --skill java-clean-general --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Java Clean General?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/caslubbers-java-clean-general)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: java-clean-general
description: Enforces general code quality in Java 21+ — DRY, no magic numbers, immutability by default, sealed hierarchies and pattern matching over instanceof chains, encapsulation over getters and setters, and streams used where they clarify. Use when reviewing or refactoring Java for quality, and when the code shows duplication, hardcoded values, long if-else or switch chains on type, mutable shared state, anaemic classes, deep call chains like a.getB().getC().getD(), or clever one-liners. Also trigger on: a class extending another to reuse one method, an override throwing UnsupportedOperationException, a hierarchy three levels deep, an abstract class standing in for a sealed set of cases, or asks like "should this extend or compose", "composition over inheritance".
---
# General clean code in Java
## DRY
Duplicated logic means every fix must be found in several places. Extract it to a method, a shared class, or a base type — once you see it a third time and the duplicates truly say the same thing.
Duplication that is coincidental is not duplication. Two rules that happen to compute the same number today, for different reasons, should stay apart.
## No magic values
```java
// Bad
if (user.age() >= 18 && order.total() > 100) { applyDiscount(order, 0.15); }
// Good
private static final int LEGAL_ADULT_AGE = 18;
private static final Money FREE_SHIPPING_THRESHOLD = Money.euros(100);
private static final BigDecimal LOYALTY_DISCOUNT = new BigDecimal("0.15");
```
Money is `BigDecimal` or a dedicated type, never `double` — binary floating point cannot represent 0.10, and the rounding error becomes a support ticket.
## Immutable by default
```java
// Good — a record is final, its components final, equals and hashCode supplied
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount);
if (amount.scale() > 2) throw new IllegalArgumentException("max 2 decimals");
}
public Money plus(Money other) { return new Money(amount.add(other.amount), currency); }
}
```
The compact constructor is where invariants are enforced — an object that cannot be constructed invalid never needs validating again. Mark fields `final` unless they must change, return `List.copyOf(...)` rather than the internal list, and prefer a new instance over a mutating setter.
## Sealed types and pattern matching over type checks
```java
// Bad — a chain that must be edited in five places when a shape is added
if (shape instanceof Circle) { ... }
else if (shape instanceof Square) { ... }
// Good — the compiler enforces exhaustiveness
public sealed interface Shape permits Circle, Square, Rectangle {}
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Rectangle r -> r.width() * r.height();
};
```
Adding a permitted subtype breaks the build at every switch that must handle it — which is what you want. Record patterns destructure in place: `case Circle(double radius) -> Math.PI * radius * radius`.
Behaviour that belongs to the type itself still belongs on the type. Use polymorphism when each case is the type's own business; use a sealed switch when the operation belongs to the caller.
## Tell, don't ask
```java
// Bad — anaemic object, logic outside it
if (account.getBalance().compareTo(amount) >= 0) {
account.setBalance(account.getBalance().subtract(amount));
}
// Good — the object enforces its own rule
account.withdraw(amount);
```
A class that is only getters and setters is a struct with ceremony. Move the behaviour that operates on its data into it, and stop exposing the internals that behaviour needs.
## Don't chain through objects
```java
// Bad — knows the whole graph; any change breaks it
order.getCustomer().getAddress().getCountry().getCode();
// Good
order.shippingCountryCode();
```
Ask the object you have for what you need, not for its neighbours.
## Streams where they clarify
```java
// Good
var activeEmails = users.stream()
.filter(User::isActive)
.map(User::email)
.toList();
```
A stream that needs a comment to follow should be a loop. Do not use `forEach` for side effects on a collection you are building, do not nest streams three deep, and do not chase cleverness — `toList()` beats `collect(Collectors.toList())`, and a plain `for` beats a contorted pipeline.
## Handle the boundaries
Null checks at the edges (`Objects.requireNonNull` in constructors), validation where data enters, empty collections rather than null, and `Optional` for absent return values. Inside a well-guarded core, stop re-checking.
## Composition over inheritance
Inheritance is permanent coupling to a parent's internals. Use it only where the subtype is
substitutable everywhere the parent is; for reuse, hold a field.
```java
// Bad — extends to borrow send(), inherits the whole SMTP surface as its own API
class EmailNotifier extends SmtpClient {
void notify(User user, String message) { send(user.email(), message); }
}
// Good — holds what it needs, exposes only notify
final class EmailNotifier {
private final SmtpClient smtp;
EmailNotifier(SmtpClient smtp) { this.smtp = smtp; }
void notify(User user, String message) { smtp.send(user.email(), message); }
}
```
The composed version takes a fake `SmtpClient` in tests and can swap transports. Signals you
extended for the wrong reason: an override that throws `UnsupportedOperationException`, an override
ignoring parameters the base requires, or a three-deep hierarchy with the behaviour in the middle.
Where subclasses were only ever standing in for a fixed set of cases, a sealed interface plus
records says it better — and the compiler checks exhaustiveness. Use an interface to share a
contract, a field to share behaviour, and `abstract class` only for genuine shared state with an
invariant to protect.
## Structure
One public class per file, named after the file. Fields private. Methods ordered so callers appear
above callees, so the file reads top-down (`java-clean-functions` covers the stepdown rule). Keep
classes focused — a class needing "and" to describe it is two classes. `final` on a class you do not
intend to be extended.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!