About this article
This article is the first deep dive in the “Application Architecture” category of the Architecture Crash Course for the Generative-AI Era series, covering class design.
One layer inside module design — at code-writing level — and the design judgment that appears most frequently in daily development. The article covers SOLID principles, inheritance vs composition, testability, design patterns, code-complexity numeric gates, and AI-era design — anchored on the core question: “is there only one reason this class would change?”
Before you read this
This article is mostly about how programs are written and structured. If IT vocabulary is unfamiliar, reading the primer "Programs and APIs" first makes it far easier to follow. You can also look anything up in the glossary as you read.
What is class design in the first place
Class design is “deciding what responsibilities each part (class) of a program has and how to combine them.”
Think of Lego bricks. If each brick (class) is small and simple, rearranging and repairing them is easy. But if you build one massive molded piece, a single broken section means throwing away the whole thing. Class design works the same way: sticking to “one class, one responsibility” keeps the blast radius of changes small, extending the code’s lifespan.
Why class design matters
What happens if class design is neglected? Fixing one feature breaks another, and each fix spawns new bugs in a chain. The “just throw it into the User class for now” code written as a junior tortures the successor five years later — a scene visible everywhere in the industry.
Class-design quality is no exaggeration to call the lifespan of the code. The shared language for this is the SOLID principles — five rules forming OO’s foundation.
SOLID principles
SOLID is the acronym Robert C. Martin (Clean Architecture’s proposer) compiled for the five OO design principles. Each looks obvious individually; observing all five produces “change-resilient code” empirically.
| Letter | Principle | Meaning |
|---|---|---|
| S | Single Responsibility | One class, one responsibility |
| O | Open/Closed | Open to extension, closed to modification |
| L | Liskov Substitution | Derived classes substitutable for base |
| I | Interface Segregation | Split interfaces fine-grained |
| D | Dependency Inversion | Depend on abstractions |
S: Single Responsibility
One class holds one responsibility. Equivalently, “there is only one reason this class would change.” A UserService holding authentication, profile updates, and email sending requires modification when any of authentication, profile, or email specs change — and each change risks breaking the others.
| ❌ Bad | ✅ Good |
|---|---|
| UserService handles auth, profile, email all together | Split into AuthService / ProfileService / NotificationService |
The number of responsibilities = the number of change axes. Narrowing to one keeps each class small, readable, testable.
O: Open/Closed
Open to extension, closed to modification. Aim for being able to add new features without modifying existing code. Branching payment methods with switch (method) requires modifying every switch when a new payment method appears — a textbook bug-introduction pattern.
Define a PaymentMethod interface; new payment methods just implement that interface. Existing code stays untouched. Polymorphism and interfaces realize this principle naturally.
L: Liskov Substitution
Derived classes should be substitutable for the base class. A design where base class Bird has fly() and Penguin overrides it to throw an exception breaks at any caller expecting Bird, violating Liskov.
Avoid this by reconsidering the inheritance — design with has-a (composition / delegation) instead of is-a. The modern best practice: “composition over inheritance.” Pick inheritance carefully.
I: Interface Segregation
Clients should not depend on methods they don’t use. Fat interfaces force impact even from spec changes to unused methods, so split per use as the principle.
IWorker { work(); eat(); sleep() } bundling “work, eat, sleep” into one interface forces classes representing robots to implement eat() and sleep(). Splitting into IWorkable / IEatable / ISleepable lets each class implement only the capabilities needed.
D: Dependency Inversion
Upper modules should not depend on lower modules. Both should depend on abstractions. The key principle in Clean Architecture: the natural dependency direction “OrderService (business logic) depends on MySQLUserRepository (DB implementation)” gets inverted.
Concretely, OrderService defines IUserRepository as an interface; MySQLUserRepository implements it. This puts business logic in a state where it doesn’t know the DB implementation. Switching DBs, mocking in tests — all handled by swapping the abstraction.
Implementation means: DI (Dependency Injection — receiving dependencies via constructor or setter) and placing the interface in the business-logic layer (Clean Architecture’s hallmark) — those are the two basics.
“Upper depends on lower” -> “lower depends on upper’s interface” is the inversion. Hence Dependency Inversion.
Composition over inheritance
In modern OO, composition is preferred over inheritance as best practice. Inheritance creates a strong coupling (is-a), so base-class changes cascade to all derived classes, and the LSP-violation trap waits.
| Approach | Trait |
|---|---|
| Inheritance | Strong coupling, LSP-violation trap, multiple-inheritance issues, hard to test |
| Composition | Loose coupling, easy to test, runtime swap, principled flexibility |
❌ class Admin extends User // Inheritance (strong coupling)
✅ class Admin { private user: User } // Composition (loose coupling)
Composition over inheritance is the rule. Inherit only for “truly variants of the same thing”; otherwise composition is more flexible.
Testability design
Easy-to-test design = good design. Hard-to-test classes have too many dependencies, vague responsibilities, or hidden side effects. Just observing these principles dramatically raises testability.
- Dependencies via constructor (DI): don’t
newdirectly; inject from outside. - Avoid global state: minimize singletons and statics.
- Separate side effects from pure logic: compute logic as pure functions, isolate I/O in thin layers.
- Confine I/O to thin layers: only boundary classes touch DB, files, external APIs.
❌ UserService news up DB / Email / Redis
✅ Inject dependencies; mock in tests
A handful of design patterns come up often enough to be worth naming.
Knowing names of recurring design patterns lowers in-team communication cost. But making pattern application the goal becomes “just adding classes — over-engineering.”
| Pattern | Use |
|---|---|
| Repository | Abstract data access |
| Factory | Hide complex creation logic |
| Strategy | Make behavior swappable |
| Adapter | Convert existing-class interface |
| Observer | Event notification, Pub/Sub (publish/subscribe messaging) |
| Decorator | Stack functionality |
Patterns are means to a goal. Making name-attaching the goal fails.
Object orientation has its own recurring traps.
Even knowing SOLID, falling into these anti-patterns at implementation is frequent. “God classes” and “anemic domain models” are most often seen when class design has collapsed.
- God Class: a giant class that knows everything. Violates Single Responsibility.
- Anemic domain model: only data, with all logic in service layer. OO in name only.
- Excessive inheritance trees: 4+ levels needs review. Sign of over-complex design.
- All-private, untestable: hard to test = sign of bad design.
- Util-class syndrome: collection of static methods. Becomes a function dump where nothing’s findable.
Making the dependencies visible is what keeps all of this checkable.
Objectively grasp class-design health via dependency visualization. Tools analyzing imports and dependency graphs auto-detect unintended dependencies and cycles.
| Language | Tool |
|---|---|
| Node.js / TypeScript | Dependency Cruiser / madge |
| Java / Kotlin | ArchUnit |
| Python | import-linter |
| Go | go-cleanarch |
Cyclic dependencies are signs of design collapse. Discover A->B->C->A and reconsider responsibility separation.
Numeric gates on code complexity
Note: industry rates as of April 2026. Periodic refresh required.
Judging “good class” vaguely fails; binding via static-analysis tools (ESLint / SonarQube / Ruff) numerically is the modern standard. Industry-adopted defaults:
| Metric | Threshold | Action on overage |
|---|---|---|
| File line count | 300 | Consider splitting |
| Method line count | 50 | Extract methods |
| Public methods per class | 10 | 2+ responsibilities mixed |
| Cyclomatic complexity | 10 | Reduce if/switch; replace with polymorphism |
| Nesting depth | 3 | Early-return / guard clauses |
| Method arguments | 3 | 4+ -> parameter object |
| Inter-class dependencies | 5 | Too much fan-out -> split responsibilities |
| Copy-paste detection | 5+ duplicate lines | DRY-extract |
These are detected by SonarQube / CodeClimate / ESLint by default; “automatic CI block on PRs” is the modern style. Code left “to fix later” always becomes debt; immediate splitting on threshold-violation is the rule.
Run numeric gates as PR-time auto-checks. Don’t rely on visual review.
Three scenarios
If you are building solo or at a startup
Keep the application of design patterns to a minimum: function-based code, early returns and the ESLint complexity check are enough. In a one-person project an elaborate class hierarchy is nothing but cognitive load. Keeping the automatic check on the numeric gates — 300 lines per file, complexity 10 — is already sufficient discipline.
If you are a small or mid-size SaaS
This is the stage for introducing the three patterns that come up most — repository, factory and strategy — plus a DI container, as shared vocabulary for the team. Make testability the design criterion (constructor injection so that mocks can be swapped in), and establish an operation where CI blocks on the complexity threshold.
If you are a large enterprise
With dozens of people touching the same codebase, it becomes worth going as far as automatic detection of layer violations through dependency visualisation (ArchUnit, Dependency Cruiser). Align the vocabulary of patterns through team training and standardise the review perspectives, or the consistency of the design loses to the headcount.
AI decision axes — Testability is AI-readiness
DI and pure functions guarantee testability of AI-generated code
Designing with dependency injection (DI) to make external dependencies injectable and writing business logic as pure functions makes testing AI-generated code easy. Pure functions have clear inputs and outputs, making it easy for AI to auto-generate test cases; with DI swapping out external dependencies, integration tests become easy to write too.
Single-responsibility classes let AI modify accurately
When each class holds only one responsibility, instructing AI to “fix the validation logic in this class” limits the blast radius. In God Classes with multiple responsibilities, there’s a risk that AI fixing one part breaks another responsibility.
Pitfalls and forbidden moves
Here are the six most dangerous ways an implementation goes wrong even when the team knows SOLID.
| Forbidden move | Why it is bad → what to do instead |
|---|---|
| Growing a God class | the accumulation of “while I am here” reaches three thousand lines → hold to one responsibility and split at 300 lines |
| An anemic domain model | entities hold only data and all the logic pools in the service layer → move logic onto entities and value objects |
| An inheritance tree four levels deep or more | the triple burden of LSP violations, change propagation and being hard to follow → restructure it as delegation |
Naming things Util, Helper or Manager | a name that states no responsibility is an empty box anything can be stuffed into → if the responsibility is clear, a concrete name follows |
Calling new in the constructor, with no DI | mocking in tests becomes impossible → inject dependencies from outside |
| Leaving circular dependencies (A → B → A) in place | a change in one class cascades → detect them automatically with import/no-cycle or the equivalent |
Fat services, anemic domains and God classes are the anti-patterns you find in many of the projects that say “we adopted DDD.” What matters is not the pattern name but asking “where does the logic actually live?”
Author’s note — a 3,000-line UserService
In an inherited project, a 3,000-line UserService contained authentication, billing, profile, email, notifications, all together. A single-line fix in an email template would break a billing test — “touch and break” state. Each new feature consumed half a day investigating “who pressing which button does what.”
Stories of inheriting similar projects: the first task is “sticky-noting what this class does”, and giving up manual splitting once stickies exceed 20. God Classes aren’t born in a day — they grow via accumulated “while we’re here, throw it in.”
The road begins the moment SOLID’s “S” is broken once; later splitting requires understanding all behaviors and rewriting. Class design is decided by “the first class’s” responsibility scope.
If you’ve grown a God Class, splitting is essentially rebuilding. Narrowing responsibility to one at the start is critical.
What you must decide — what’s your project’s answer?
Articulate your project’s answer in 1-2 sentences for each:
- Class-granularity guidance (how strictly to apply Single Responsibility)
- Dependency-injection method (constructor / DI container)
- Inheritance vs composition default
- Interface ownership layer (when adopting Clean)
- Package / namespace boundaries
- Test granularity and coverage targets
Related Articles
Summary
This article covered class design — SOLID, inheritance vs composition, testability, code-complexity numeric gates.
Always ask “is there one reason this class would change?”, composition over inheritance, separate side effects via DI. The 2026 realistic answer for class design including AI era.
The next article covers domain logic (Transaction Script vs DDD, Value Object, aggregates).
Back to series TOC -> ‘Architecture Crash Course for the Generative-AI Era’: How to Read This Book
I hope you’ll read the next article as well.
Also popular with readers
📚 Series: Architecture Crash Course for the Generative-AI Era (32/95)