Skip to main content
the readable codebase

The Package with Forty Classes

5 min read Chapter 14 of 27

The Package with Forty Classes

The Smell

The logistics platform’s com.logitrack.service package contains 67 classes. Opening this package in any IDE presents a wall of files. The developer looking for shipment tracking logic must scan past AccountingService, AuditService, BillingReconciliationService, BulkImportService, and 25 more classes before reaching ShipmentTrackingService. The alphabetical ordering places unrelated classes next to each other. CarrierRateService sits next to CustomerNotificationService. InvoiceService sits next to InventoryAllocationService.

The package provides no navigation aid. It is a directory, not an organizational unit. A new developer on the team asks: “Which services are related to billing?” The only way to answer is to read every service class name and make a judgment call about which ones sound billing-related.

The Cognitive Cost

A package with 67 classes requires the developer to make 67 relevance decisions when navigating: “Is this class related to my task?” Each decision is cheap individually but the accumulated cost is significant. Developers learn to use full-text search instead of navigating the package tree, which means the package structure has stopped providing value.

When a developer uses search instead of navigation, they lose the spatial context that good package structure provides. They find the class they need, but they do not see its neighbors. They do not discover that ShipmentTrackingService and TrackingEventPublisher are related and might both need changes. They do not see that CarrierWebhookHandler belongs to the same feature as CarrierRateService. Search finds one thing at a time. Structure shows relationships.

The Before

// HARD TO READ: 67 classes in one package. No grouping. No structure.
// A developer must scan every class name to find related classes.

package com.logitrack.service;

// Billing-related (scattered among 67 files)
public class BillingService { /* ... */ }
public class BillingReconciliationService { /* ... */ }
public class InvoiceService { /* ... */ }
public class PaymentProcessingService { /* ... */ }
public class TaxCalculationService { /* ... */ }

// Shipment-related (scattered among 67 files)
public class ShipmentCreationService { /* ... */ }
public class ShipmentTrackingService { /* ... */ }
public class ShipmentStatusService { /* ... */ }
public class ShipmentLabelService { /* ... */ }
public class ShipmentRateService { /* ... */ }

// Warehouse-related (scattered among 67 files)
public class WarehouseService { /* ... */ }
public class InventoryAllocationService { /* ... */ }
public class PickingService { /* ... */ }
public class StockCountService { /* ... */ }

// Carrier-related (scattered among 67 files)
public class CarrierIntegrationService { /* ... */ }
public class CarrierRateService { /* ... */ }
public class CarrierWebhookHandler { /* ... */ }

// Utility-type services (scattered among 67 files)
public class AuditService { /* ... */ }
public class NotificationService { /* ... */ }
public class ReportingService { /* ... */ }
// ... 47 more classes

The Fix

// READABLE: Each package groups related classes.
// A developer working on billing sees only billing classes.
// Navigation replaces search.

package com.logitrack.billing;

// Only billing classes. A developer opens this package and sees
// everything related to billing. Nothing else.
public class InvoiceGenerator { /* ... */ }
public class BillingReconciler { /* ... */ }
public class PaymentProcessor { /* ... */ }
class TaxCalculator { /* package-private: used only within billing */ }
package com.logitrack.shipment.tracking;

// Only tracking classes. A developer modifying tracking logic
// sees its immediate neighbors and knows which classes might need changes.
public class TrackingStatusUpdater { /* ... */ }
public class TrackingEventPublisher { /* ... */ }
class TrackingEventMapper { /* package-private: internal implementation */ }
package com.logitrack.carrier;

// Only carrier integration classes.
public class CarrierGateway { /* ... */ }
public class CarrierRateCalculator { /* ... */ }
class CarrierWebhookHandler { /* package-private: internal detail */ }

The split decision is driven by one question: which classes change together? Analysis of the git history for the logistics platform shows that InvoiceGenerator, BillingReconciler, and PaymentProcessor appear in the same commits 73% of the time. ShipmentTrackingService and TrackingEventPublisher appear together 81% of the time. Classes that change together belong together. Classes that change independently belong in separate packages.

After the restructuring, the largest package has 8 classes. A developer opening any package sees only related classes. Navigation works again. The package tree is a map of the system’s capabilities, not an alphabetical list of every service class ever written.

The Rule

If a package has more than 15 classes, split it by co-change frequency: group classes that appear in the same git commits. Use git log --name-only to identify clusters. A package should be small enough that a developer can scan every file name in two seconds and know whether their change belongs here. If they must scroll, the package is too large. Name the sub-packages after business capabilities, not technical layers.