Loading learning content...
Of all the misunderstandings surrounding the Single Responsibility Principle, none is more pervasive—or more destructive—than the belief that SRP means each class should have only one method.
This interpretation sounds reasonable on the surface. After all, if a class should have 'one responsibility,' wouldn't that translate to one action, and therefore one method? The logic seems airtight.
Except it's completely wrong.
This misconception has spawned countless 'enterprise-grade' codebases where simple operations are fragmented across dozens of anemic classes, each containing a single method. The result is code that's harder to understand, harder to maintain, and ironically, harder to change—the very opposite of what SRP was designed to achieve.
Developers who misinterpret SRP as 'one method per class' often create systems with hundreds of tiny classes that are impossible to navigate. The cure becomes worse than the disease, and teams eventually abandon any attempt at principled design. This page will help you avoid that fate.
By the end of this page, you will understand the precise relationship between methods, responsibilities, and reasons to change. You'll be equipped to recognize when a class with many methods still honors SRP, and when a class with few methods violates it.
To understand why this misconception is so widespread, we need to trace its origins. The confusion stems from conflating two fundamentally different concepts: methods and responsibilities.
The linguistic trap:
When we say a class should have 'one responsibility,' natural language suggests a single task—something atomic and indivisible. But Robert C. Martin's original formulation was far more nuanced:
"A class should have only one reason to change."
Notice the emphasis isn't on what the class does (its methods), but on what might cause it to change (its reason to change). These are completely different dimensions of analysis.
| Concept | What It Describes | Quantity in Well-Designed Class |
|---|---|---|
| Methods | Actions the class can perform | As many as needed for cohesion |
| Responsibility | The conceptual purpose the class serves | One cohesive area |
| Reason to Change | External force that would require modification | One stakeholder or concern |
The actor model clarification:
In later writings, Martin refined his definition to make it even clearer:
"A module should be responsible to one, and only one, actor."
An 'actor' is a stakeholder—a person or group who might request changes. A class that reports employee data might have methods for calculatePay(), generatePayslip(), and exportToAccounting(). If all these methods serve the Accounting department's needs, the class has one reason to change (Accounting requirements change) despite having multiple methods.
Conversely, if calculatePay() serves Accounting but reportHours() serves Operations, you have two actors—and two reasons to change—regardless of having only two methods.
Stop counting methods. Start identifying stakeholders. The question isn't 'How many things does this class do?' but 'Who would request changes to this class?' If multiple stakeholders with conflicting interests could request changes, you likely have an SRP violation.
A class with a single responsibility will often have multiple methods that work together to fulfill that responsibility. These methods are cohesive—they operate on the same data, serve the same purpose, and change for the same reasons.
Consider a ShoppingCart class:
1234567891011121314151617181920212223242526
public class ShoppingCart { private List<CartItem> items; private DiscountStrategy discountStrategy; // Adding and removing items - all serve the same responsibility public void addItem(Product product, int quantity) { ... } public void removeItem(String productId) { ... } public void updateQuantity(String productId, int newQuantity) { ... } public void clearCart() { ... } // Querying cart state - still same responsibility public List<CartItem> getItems() { ... } public int getItemCount() { ... } public boolean containsProduct(String productId) { ... } public CartItem findItem(String productId) { ... } // Calculating totals - same responsibility public Money getSubtotal() { ... } public Money getDiscount() { ... } public Money getTax() { ... } public Money getTotal() { ... } // Cart lifecycle - same responsibility public boolean isEmpty() { ... } public void applyDiscount(DiscountStrategy strategy) { ... }}This class has 17 methods, yet it adheres perfectly to SRP. Why?
items and serves cart managementSplitting this into 17 classes—CartItemAdder, CartItemRemover, CartQuantityUpdater, etc.—would be absurd. You'd destroy the cohesion that makes the code understandable.
High cohesion means the methods of a class are strongly related—they use the same instance variables, work toward the same goal, and change together. SRP and cohesion are complementary: a class with a single responsibility should exhibit high cohesion, and highly cohesive classes naturally tend to have single responsibilities.
Teams that fall into the 'one method per class' trap exhibit distinctive symptoms. Recognizing these patterns can help you diagnose and correct the problem before it spreads.
DiscountCalculator, DiscountValidator, DiscountApplier, DiscountLogger, etc.The ironic outcome:
The goal of SRP is to make code easier to change by limiting the impact of modifications. But one-method-per-class achieves the opposite:
12345678910111213141516171819202122232425262728293031323334353637383940
// ❌ One-method-per-class nightmarepublic class OrderTotalCalculator { public Money calculate(Order order) { return new OrderSubtotalCalculator().calculate(order) .add(new OrderTaxCalculator().calculate(order)) .subtract(new OrderDiscountCalculator().calculate(order)) .add(new OrderShippingCalculator().calculate(order)); }} public class OrderSubtotalCalculator { public Money calculate(Order order) { ... }} public class OrderTaxCalculator { public Money calculate(Order order) { ... }} public class OrderDiscountCalculator { public Money calculate(Order order) { ... }} public class OrderShippingCalculator { public Money calculate(Order order) { ... }} // ✅ Cohesive approach - all pricing in one placepublic class OrderPricingCalculator { public Money calculateTotal(Order order) { return calculateSubtotal(order) .add(calculateTax(order)) .subtract(calculateDiscount(order)) .add(calculateShipping(order)); } private Money calculateSubtotal(Order order) { ... } private Money calculateTax(Order order) { ... } private Money calculateDiscount(Order order) { ... } private Money calculateShipping(Order order) { ... }}When tempted to split a class, ask: 'Would these parts ever change for different reasons?' If they always change together—like tax, discount, and shipping calculations when pricing rules change—keep them together. Separation without cause is fragmentation, not design.
A more productive mental model is to think of methods as facets of a single responsibility—different angles from which you interact with one cohesive concept.
Consider a UserAuthentication class:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
public class UserAuthentication { private final PasswordEncoder passwordEncoder; private final TokenGenerator tokenGenerator; private final UserRepository userRepository; // All methods serve the single responsibility of "authenticating users" public AuthResult login(String username, String password) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new AuthenticationException("User not found")); if (!verifyPassword(user, password)) { recordFailedAttempt(user); throw new AuthenticationException("Invalid password"); } return createAuthResult(user); } public void logout(String token) { tokenGenerator.invalidate(token); } public AuthResult refreshToken(String refreshToken) { // Validate and issue new token } public boolean validateToken(String token) { return tokenGenerator.validate(token); } public void changePassword(User user, String oldPassword, String newPassword) { if (!verifyPassword(user, oldPassword)) { throw new AuthenticationException("Current password incorrect"); } user.setPasswordHash(passwordEncoder.encode(newPassword)); userRepository.save(user); } public void resetPassword(String email) { // Initiate password reset flow } // Private helper methods - implementation details of the responsibility private boolean verifyPassword(User user, String password) { return passwordEncoder.matches(password, user.getPasswordHash()); } private void recordFailedAttempt(User user) { // Track failed login for security } private AuthResult createAuthResult(User user) { String accessToken = tokenGenerator.generateAccess(user); String refreshToken = tokenGenerator.generateRefresh(user); return new AuthResult(accessToken, refreshToken, user.toProfile()); }}This class has 9+ methods, including public and private. Yet the responsibility is singular: user authentication.
Analyzing the facets:
| Method | Facet of Authentication |
|---|---|
login() | Primary authentication flow |
logout() | Session termination |
refreshToken() | Session extension |
validateToken() | Session verification |
changePassword() | Credential management |
resetPassword() | Credential recovery |
Each method represents a different aspect of the same responsibility. The Security team owns all these methods. They would all change if authentication requirements change (e.g., adding MFA). They're cohesive—operating on the same concepts (users, passwords, tokens).
Think of a class as a gem with multiple facets. Each method is a facet—a different way light enters the same crystal. The gem (responsibility) is singular; the facets (methods) are many. SRP doesn't limit facets; it ensures they all belong to the same gem.
While method count isn't the measure of SRP compliance, it can serve as a warning signal that merits investigation. A class with 50 methods might be perfectly focused, but it's statistically more likely to harbor hidden responsibilities.
High method count as a smell (not a violation):
methodA() uses fields X and Y while methodB() uses fields Z and W with no overlap, cohesion is lowThe investigation process:
When you encounter a large class, don't immediately split it. Instead:
git log to see if methods change together or independentlyIf clusters emerge with distinct stakeholders, consider separation. If all methods orbit the same concepts and serve the same actors, the class may simply have a rich interface for a focused responsibility.
Production codebases are filled with classes that have many methods yet perfect SRP compliance. Understanding these examples builds intuition for responsible design.
Example 1: Java's ArrayList
The ArrayList class in Java has 60+ methods: add, remove, get, set, size, contains, indexOf, forEach, iterator, sort, toArray, and many more. Yet it has one responsibility: managing a dynamic, ordered collection of elements.
12345678910111213141516171819202122232425262728293031323334353637383940
// ArrayList has 60+ methods but ONE responsibility:// "Managing a resizable array-backed list" public class ArrayList<E> implements List<E> { private Object[] elementData; private int size; // Adding elements - facet of list management public boolean add(E e) { ... } public void add(int index, E element) { ... } public boolean addAll(Collection<? extends E> c) { ... } // Removing elements - facet of list management public E remove(int index) { ... } public boolean remove(Object o) { ... } public void clear() { ... } // Accessing elements - facet of list management public E get(int index) { ... } public E set(int index, E element) { ... } // Querying - facet of list management public int size() { ... } public boolean isEmpty() { ... } public boolean contains(Object o) { ... } public int indexOf(Object o) { ... } // Iteration - facet of list management public Iterator<E> iterator() { ... } public void forEach(Consumer<? super E> action) { ... } // Transformation - facet of list management public Object[] toArray() { ... } public void sort(Comparator<? super E> c) { ... } // ... and 40+ more methods} // All 60+ methods serve ONE actor: developers using the List abstraction// All methods share ONE reason to change: List semantics evolveExample 2: A Domain-Rich Entity
Consider an Invoice class in an accounting system:
12345678910111213141516171819202122232425262728293031323334353637383940
public class Invoice { private final InvoiceId id; private final CustomerId customerId; private final List<LineItem> lineItems; private InvoiceStatus status; private Money amountPaid; private LocalDate dueDate; private final LocalDate issueDate; // Lifecycle management public void finalize() { this.status = InvoiceStatus.FINALIZED; } public void send() { this.status = InvoiceStatus.SENT; } public void markOverdue() { this.status = InvoiceStatus.OVERDUE; } public void archive() { this.status = InvoiceStatus.ARCHIVED; } // Line item management public void addLineItem(LineItem item) { ... } public void removeLineItem(LineItemId itemId) { ... } public void updateLineItem(LineItemId itemId, LineItem updated) { ... } // Financial calculations public Money getSubtotal() { ... } public Money getTaxAmount() { ... } public Money getTotal() { ... } public Money getAmountDue() { return getTotal().subtract(amountPaid); } // Payment processing public void recordPayment(Money amount) { ... } public boolean isPaid() { return getAmountDue().isZero(); } public boolean isPartiallyPaid() { ... } // Status queries public boolean isEditable() { return status == InvoiceStatus.DRAFT; } public boolean isOverdue() { return !isPaid() && LocalDate.now().isAfter(dueDate); } public boolean canBeSent() { ... } // Presentation public String getFormattedTotal() { ... } public String getStatusDescription() { ... }}This Invoice class has 20+ methods covering lifecycle, line items, calculations, payments, status, and presentation. Yet the responsibility is singular: representing and managing an invoice.
The Billing/Accounting team owns this class. All methods serve the same domain concept. This is rich domain modeling, not SRP violation.
In Domain-Driven Design, entities naturally accumulate methods that represent business behaviors. An Invoice that can be finalized, sent, paid, and queried is a rich domain object—not an SRP violation. The key is that all behaviors relate to the 'Invoice' aggregate concept.
Instead of counting methods, apply the Reason to Change Analysis—a systematic way to evaluate SRP compliance regardless of method count.
The Three-Question Test:
calculateTax() but not sendReminder(), these might be different responsibilities.12345678910111213141516171819202122232425262728293031323334
// ❌ FAILS the Three-Question Testpublic class ReportService { // Actor 1: Finance team public Report generateFinancialReport() { ... } public void exportToExcel(Report report) { ... } // Actor 2: Marketing team public Report generateMarketingReport() { ... } public void exportToSlides(Report report) { ... } // Actor 3: IT Operations public void scheduleReport(Report report, Schedule schedule) { ... } public void monitorReportJobs() { ... }} // Analysis:// Q1: Three different actors (Finance, Marketing, IT)// Q2: Financial report changes don't affect marketing reports// Q3: "Generates and schedules financial and marketing reports" - multiple 'and's // ✅ PASSES the Three-Question Testpublic class FinancialReportGenerator { public Report generate(DateRange period, ReportOptions options) { ... } public Report generateYTD() { ... } public Report generateQuarterly(Quarter quarter) { ... } public Summary generateExecutiveSummary() { ... } public void exportToExcel(Report report, OutputStream out) { ... } public void exportToPDF(Report report, OutputStream out) { ... }} // Analysis:// Q1: One actor (Finance team)// Q2: All methods change when financial reporting requirements change// Q3: "Generates financial reports in various formats" - one sentence, no 'and'The failing example has 6 methods; the passing example also has 6 methods. Method count is identical, but SRP compliance is opposite. This proves definitively that SRP is not about counting methods—it's about analyzing cohesion and change drivers.
We've dismantled one of the most pervasive SRP misconceptions. Let's consolidate the key insights:
What's next:
Having cleared up the 'one method per class' myth, we'll tackle another common misconception: the belief that SRP requires tiny classes everywhere. As we'll see, this interpretation leads to a different but equally problematic form of over-engineering.
You now understand that SRP is about cohesion and change drivers, not method counting. The goal is focused classes that change for one reason—regardless of how many methods that focus requires.