Use when modelling a rich domain in Java - SOLID principles, encapsulated entities that enforce their own invariants, and value objects over primitives
Installs into .claude/skills of the current project.
Are you the author of Java Oop Solid Design?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/makifbaysal-java-oop-solid-design)
---
name: java-oop-solid-design
category: architecture
description: Use when modelling a rich domain in Java - SOLID principles, encapsulated entities that enforce their own invariants, and value objects over primitives
tech_stack: Java
---
# Java OOP & SOLID Design
## Overview
Java is chosen precisely when the domain is rich (see java-vs-go-decision). That value is only realized if you model it with real objects that protect their invariants — not anemic data bags with a pile of setters and logic scattered in services.
**Core principle:** Objects own their invariants. If a rule about an entity can be violated from outside the entity, the model is broken.
## SOLID, concretely
- **S — Single Responsibility:** one reason to change per class. A class that parses HTTP, applies business rules, and writes SQL is three classes.
- **O — Open/Closed:** extend behavior via new types/strategies, not by editing a growing `switch`. New payment method → new `PaymentMethod` implementation, not another `case`.
- **L — Liskov:** a subtype must honor the supertype's contract. If `Square extends Rectangle` breaks `setWidth`, the hierarchy is wrong — prefer composition.
- **I — Interface Segregation:** many small role interfaces over one fat one. A caller that needs `read` shouldn't depend on `write`.
- **D — Dependency Inversion:** services depend on interfaces (ports), not concrete adapters. Inject the interface via the constructor.
## Encapsulation over anemic models
```java
// ❌ anemic: invariant lives nowhere, anyone can break it
class Task {
public String title; // no bound enforced
public Status status;
}
task.title = "x".repeat(500); // invalid state, no guard
// ✅ rich: the entity enforces its own rules
public final class Task {
private final TaskId id;
private String title;
private Status status;
private Task(TaskId id, String title) { this.id = id; this.title = title; }
public static Task create(String title) {
if (title == null || title.isBlank() || title.length() > 200)
throw new InvalidTaskTitle(title);
return new Task(TaskId.newId(), title);
}
public void rename(String title) { /* same guard, reused */ }
public void complete() {
if (status == Status.DONE) throw new IllegalTransition("already done");
this.status = Status.DONE;
}
// getters, no public setters
}
```
## Value objects over primitives
Wrap meaningful primitives: `TaskId`, `Email`, `Money` — not raw `String`/`long`/`BigDecimal`. A `TaskId` can't be accidentally passed where a `UserId` is expected, and validation lives in one place. Java `record` makes these cheap.
## Prefer
- `record` for immutable value objects and DTOs.
- `sealed` interfaces + pattern matching for closed hierarchies (states, commands).
- Immutability by default; expose behavior methods, not setters.
- Throw domain exceptions (`InvalidTaskTitle`) mapped to HTTP status at the boundary — not raw `IllegalArgumentException` leaking out.
Prefer a `sealed` interface + exhaustive `switch` pattern matching (final since Java 21) for a closed set of states or commands over a type enum plus `if`/`instanceof` chains — the compiler then refuses to let a new case go unhandled.
## Common Mistakes
- Public setters that let callers build invalid state.
- Business rules in the service that should be on the entity.
- `String`/`long` everywhere instead of value objects.
- A god-service with dozens of methods and no domain objects.
- Lombok `@Data`/`@EqualsAndHashCode` on a JPA entity — it pulls in lazy associations (forcing a load, or crashing outside the session) and generates a mutable, settable id that breaks `equals`/`hashCode` once the entity is persisted. Write `equals`/`hashCode` on the id only, or skip Lombok on entities.
## Red Flags
- An entity with only getters/setters and no behavior.
- The same validation copy-pasted in two services → it belongs on the entity/value object.
- A `switch` over a type enum that grows every feature → missing polymorphism.