Loading learning content...
"How many lines of code should a class have?"
This is one of the most frequently asked questions about clean class design, and it's the wrong question. Lines of code are a symptom, not the disease. A 500-line class might be perfectly well-designed if those lines form a cohesive whole. A 50-line class might be a maintenance nightmare if it tangles unrelated concerns.
Yet size does matter—just not in the simplistic "lines of code" sense. Appropriate size is about cognitive load: can a developer hold the entire class in their mind at once? This page explores what appropriate size truly means and how to achieve it.
By the end of this page, you will understand the multiple dimensions of class size, recognize when a class has grown too large or been over-decomposed, and possess practical techniques for sizing classes appropriately.
Class size is multidimensional. A class can be 'too large' in several independent ways, each requiring different remedies. Understanding these dimensions helps you diagnose specific problems rather than applying blanket rules.
Date class).| Dimension | Green Zone | Yellow Zone | Red Zone |
|---|---|---|---|
| Lines of Code | < 200 | 200-500 | 500 |
| Number of Methods | < 10 | 10-20 | 20 |
| Instance Variables | < 5 | 5-10 | 10 |
| Cyclomatic Complexity (per method) | < 5 | 5-10 | 10 |
| Max Nesting Depth | 2-3 | 4 | 4 |
These thresholds are heuristics, not laws. A 600-line parser for a complex grammar might be appropriate. A 150-line 'utility' class mixing unrelated functions is not. Always consider context: is the size justified by genuine complexity, or is it accidental complexity that could be reduced?
Beyond raw metrics, certain patterns in how you work with a class reveal that it has grown too large. These are experiential signals—things you feel while developing and maintaining the code.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
/** * WARNING: This is an anti-pattern known as a "God Class" * * Signs of excessive size: * - 47 instance variables tracking unrelated state * - 89 methods spanning multiple responsibilities * - 4,200 lines of code * - 23 direct dependencies (constructor nightmare) * - Average method: 65 lines with deep nesting */public class OrderManagementSystem { // Customer data section (lines 1-400) private Customer customer; private Address shippingAddress; private Address billingAddress; private PaymentMethod paymentMethod; // ... 15 more customer-related fields // Inventory data section (lines 401-800) private List<Product> products; private Map<String, Integer> stockLevels; private Warehouse primaryWarehouse; // ... 12 more inventory fields // Order data section (lines 801-1200) private Order currentOrder; private List<OrderItem> items; private PricingStrategy pricing; // ... 10 more order fields // Shipping data section (lines 1201-1600) private Carrier carrier; private TrackingInfo tracking; // ... 8 more shipping fields // Payment processing (lines 1601-2500) - 900 LINES public PaymentResult processPayment() { /* complex payment logic */ } public void handleRefund() { /* refund logic */ } public void reconcilePayment() { /* reconciliation */ } // Order management (lines 2501-3200) - 700 LINES public void createOrder() { /* creation logic */ } public void modifyOrder() { /* modification */ } public void cancelOrder() { /* cancellation */ } // Inventory management (lines 3201-3800) - 600 LINES public void checkStock() { /* stock checking */ } public void reserveInventory() { /* reservation */ } public void releaseInventory() { /* release */ } // Shipping orchestration (lines 3801-4200) - 400 LINES public void scheduleShipment() { /* scheduling */ } public void trackShipment() { /* tracking */ } // This class should be at least 8 separate classes}The opposite problem—over-decomposition—creates 'class explosion.' When classes are too small, the system's structure becomes obscured by a blizzard of trivial types. Finding where logic lives becomes a treasure hunt through dozens of near-empty classes.
OrderValidator, OrderValidatorHelper, OrderValidationRunner, OrderValidationResult, OrderValidationContext for what should be one concern.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// ❌ BAD: Over-decomposition - these should be ONE class class EmailValidator { private final EmailSyntaxChecker syntaxChecker; private final EmailDomainValidator domainValidator; public boolean validate(String email) { return syntaxChecker.check(email) && domainValidator.validate(email); }} class EmailSyntaxChecker { public boolean check(String email) { return email.contains("@"); // One line of actual logic }} class EmailDomainValidator { private final DomainExistenceVerifier verifier; public boolean validate(String email) { return verifier.verify(extractDomain(email)); } private String extractDomain(String email) { return email.split("@")[1]; // One line of actual logic }} class DomainExistenceVerifier { public boolean verify(String domain) { // DNS lookup - the only substantial logic }} // ✅ BETTER: Consolidated into one cohesive class class EmailValidator { public ValidationResult validate(String email) { if (!hasSyntax(email)) { return ValidationResult.invalid("Missing @ symbol"); } String domain = extractDomain(email); if (!domainExists(domain)) { return ValidationResult.invalid("Domain does not exist"); } return ValidationResult.valid(); } private boolean hasSyntax(String email) { return email.contains("@") && email.indexOf("@") > 0; } private String extractDomain(String email) { return email.split("@")[1]; } private boolean domainExists(String domain) { // DNS lookup }}If separating two pieces of code means they constantly need to call each other, or one is meaningless without the other, they probably belong together. Cohesion trumps small-for-small's-sake.
Between the god class and class explosion lies the Goldilocks zone—classes that are 'just right.' These classes are small enough to understand at a glance but large enough to contain meaningful, cohesive functionality.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
/** * Calculates shipping costs for orders. * * This class is well-sized because: * - Single, clear responsibility (shipping cost calculation) * - Reasonable number of methods (5 public, 3 private) * - Limited state (3 dependencies) * - Each method is focused and testable * - Total: ~80 lines of meaningful code */public class ShippingCalculator { private final WeightPricingTable weightPricing; private final DistanceCalculator distanceCalculator; private final CarrierRates carrierRates; public ShippingCalculator( WeightPricingTable weightPricing, DistanceCalculator distanceCalculator, CarrierRates carrierRates ) { this.weightPricing = weightPricing; this.distanceCalculator = distanceCalculator; this.carrierRates = carrierRates; } // Public API: Clear, focused methods public Money calculateStandardShipping(Order order) { Weight totalWeight = calculateTotalWeight(order); Distance distance = calculateDistance(order); return computeCost(totalWeight, distance, ShippingTier.STANDARD); } public Money calculateExpressShipping(Order order) { Weight totalWeight = calculateTotalWeight(order); Distance distance = calculateDistance(order); return computeCost(totalWeight, distance, ShippingTier.EXPRESS); } public Money calculateOvernightShipping(Order order) { Weight totalWeight = calculateTotalWeight(order); Distance distance = calculateDistance(order); return computeCost(totalWeight, distance, ShippingTier.OVERNIGHT); } public List<ShippingOption> getAllOptions(Order order) { return List.of( new ShippingOption(ShippingTier.STANDARD, calculateStandardShipping(order)), new ShippingOption(ShippingTier.EXPRESS, calculateExpressShipping(order)), new ShippingOption(ShippingTier.OVERNIGHT, calculateOvernightShipping(order)) ); } public boolean isEligibleForFreeShipping(Order order) { return order.subtotal().isGreaterThan(Money.of(75)) && calculateTotalWeight(order).isLessThan(Weight.pounds(50)); } // Private helpers: Implementation details private Weight calculateTotalWeight(Order order) { return order.items().stream() .map(item -> item.product().weight().times(item.quantity())) .reduce(Weight.ZERO, Weight::add); } private Distance calculateDistance(Order order) { return distanceCalculator.between( order.warehouse().location(), order.shippingAddress() ); } private Money computeCost(Weight weight, Distance distance, ShippingTier tier) { Money baseCost = weightPricing.priceFor(weight); Money distanceCost = carrierRates.rateFor(distance, tier); return baseCost.add(distanceCost); }}Rather than measuring lines of code, use these practical strategies to right-size classes during design and refactoring:
A command-line utility might have a 30-line main class handling all logic. An enterprise application might have 300-line classes for complex domain rules. Neither is inherently wrong. Size appropriateness depends on domain complexity, team conventions, and system requirements.
When you've identified a class that's grown too large, systematic refactoring reduces it to appropriate size. The key is incremental extraction—pulling out cohesive pieces one at a time while maintaining working software.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// BEFORE: 400-line class with payment and notification mixed class OrderProcessor { // Fields for order processing private OrderRepository orderRepository; private InventoryService inventoryService; // Fields for payment (extraction candidate) private PaymentGateway paymentGateway; private FraudDetector fraudDetector; private TransactionLogger transactionLogger; // Fields for notification (extraction candidate) private EmailService emailService; private SmsService smsService; private NotificationPreferences preferences; public void processOrder(Order order) { // Order logic: ~100 lines // Payment logic: ~150 lines (cluster 1) // Notification logic: ~100 lines (cluster 2) }} // AFTER: Three focused classes class OrderProcessor { private final OrderRepository orderRepository; private final InventoryService inventoryService; private final PaymentProcessor paymentProcessor; // extracted private final OrderNotifier orderNotifier; // extracted public void processOrder(Order order) { validateAndReserve(order); paymentProcessor.processPayment(order); orderNotifier.notifyOrderPlaced(order); }} class PaymentProcessor { // Extracted from OrderProcessor private final PaymentGateway paymentGateway; private final FraudDetector fraudDetector; private final TransactionLogger transactionLogger; public PaymentResult processPayment(Order order) { // Payment logic: focused, testable, ~100 lines }} class OrderNotifier { // Extracted from OrderProcessor private final EmailService emailService; private final SmsService smsService; private final NotificationPreferences preferences; public void notifyOrderPlaced(Order order) { // Notification logic: focused, testable, ~80 lines }}Certain classes legitimately exceed typical size guidelines. Recognizing these exceptions prevents unnecessary refactoring that would actually harm the design.
Date or Money class might have 50+ methods (arithmetic, comparison, formatting, parsing) yet remain cohesive. Each method operates on the same core concept.Matrix class or ComplexNumber class might have many operations that all operate on the same mathematical structure.A large class is acceptable if every method intimately operates on the same core state and concept. The question isn't 'how many lines?' but 'do all these methods belong together?' If yes, size is justified.
Class size is about cognitive manageability, not arbitrary line counts. Well-sized classes can be understood in their entirety and changed with confidence.
What's next:
Size is closely related to the next topic: cohesive members. A well-sized class achieves that size through cohesion—every member (field and method) working together toward the same purpose. The next page explores what cohesion means and how to achieve it.
You now understand the multi-dimensional nature of class size and how to find the Goldilocks zone. Next, we'll explore cohesion—the glue that binds class members into a unified whole.