Skip to main content
the readable codebase

The Everything-Is-Public Codebase

5 min read Chapter 17 of 27

The Everything-Is-Public Codebase

The Smell

A new developer joins the logistics platform team and is asked to integrate a new carrier. They look at the carrier package and see 11 classes, all public. Which ones should they use? Which ones are internal implementation details? CarrierGateway sounds like a public API. CarrierConnectionPool sounds internal. CarrierResponseMapper could go either way. CarrierRateNormalizer sounds internal. The developer has no way to distinguish the package’s intended API from its implementation without reading every class or asking a teammate.

In the logistics codebase, 338 of 338 classes are public. The ratio of public to package-private classes is a measure of boundary enforcement. A well-designed package exposes 2 to 4 classes as its public API and keeps the rest package-private. The logistics platform exposes everything.

The Cognitive Cost

When every class is public, a developer working in the billing package can import any class from the carrier package. They might import CarrierGateway, the intended API. They might import CarrierConnectionPool, an internal detail that could change without notice. They might import CarrierResponseMapper, creating a dependency on the carrier package’s internal data transformation logic.

Each unnecessary public class adds one possible dependency that could be created. With 338 public classes, the number of possible inter-class dependencies is $338 \times 337 = 113{,}906$. If 200 of those classes were package-private, the number of possible inter-class dependencies drops to $138 \times 137 = 18{,}906$. The theoretical coupling surface is reduced by 83%. The actual number of dependencies drops less dramatically, but every dependency that cannot exist is a dependency that cannot create cognitive load.

The Before

// HARD TO READ: All 11 classes in the carrier package are public.
// A developer in another package can depend on any of them.
// Nothing distinguishes the API from the implementation.

package com.logitrack.carrier;

public class CarrierGateway { /* Main entry point for carrier operations */ }
public class CarrierRateCalculator { /* Calculates rates from carrier APIs */ }
public class CarrierConnectionPool { /* HTTP connection pool management */ }
public class CarrierResponseMapper { /* Maps carrier API responses to domain objects */ }
public class CarrierRateNormalizer { /* Normalizes rates across different carriers */ }
public class CarrierCredentialStore { /* Stores API keys for each carrier */ }
public class CarrierCircuitBreaker { /* Circuit breaker for carrier API calls */ }
public class CarrierRetryPolicy { /* Retry configuration for carrier calls */ }
public class UpsAdapter { /* UPS-specific API adapter */ }
public class FedExAdapter { /* FedEx-specific API adapter */ }
public class DhlAdapter { /* DHL-specific API adapter */ }

A developer in the billing package writes:

import com.logitrack.carrier.CarrierConnectionPool;

// This compiles. It should not. Billing has no business knowing
// how the carrier package manages HTTP connections.

The Fix

// READABLE: Only the API surface is public.
// Implementation details are package-private.
// A developer in another package sees only what they should use.

package com.logitrack.carrier;

// PUBLIC API: These are the only classes other packages should use.
public class CarrierGateway {
    // Delegates to internal classes. Other packages call this.
    public ShippingQuote getQuote(ShipmentRoute route, CarrierId carrier) { /* ... */ }
    public TrackingInfo getTracking(TrackingNumber number) { /* ... */ }
    public ShipmentLabel generateLabel(Shipment shipment) { /* ... */ }
}

public sealed interface CarrierEvent permits
        CarrierQuoteReceived, CarrierTrackingUpdated, CarrierError {
    // Domain events that other packages can listen to
}

// PACKAGE-PRIVATE: Internal implementation. Cannot be imported by other packages.
class CarrierRateCalculator {
    // Only CarrierGateway calls this. The compiler ensures it.
}

class CarrierConnectionPool {
    // Internal HTTP management. Invisible to billing, shipment, warehouse.
}

class CarrierResponseMapper {
    // Internal data transformation. Not part of the public contract.
}

class CarrierCredentialStore {
    // Internal credential management. Not accessible from outside.
}

class CarrierCircuitBreaker {
    // Internal resilience logic. Not part of the API.
}

class CarrierRetryPolicy {
    // Internal retry configuration.
}

class UpsAdapter {
    // Carrier-specific adapter. Internal to the carrier package.
}

class FedExAdapter {
    // Carrier-specific adapter. Internal to the carrier package.
}

class DhlAdapter {
    // Carrier-specific adapter. Internal to the carrier package.
}

The carrier package now has 2 public types: CarrierGateway and CarrierEvent. A developer in another package sees exactly two options when they type import com.logitrack.carrier.. The IDE autocomplete does not suggest CarrierConnectionPool because it is not public. The compiler prevents the dependency from being created. The boundary is enforced by the language, not by convention, not by documentation, not by hope.

The audit process for the logistics codebase:

  1. For each package, list all classes.
  2. For each class, check if it is referenced by any class outside the package. Use IntelliJ’s “Find Usages” or grep for import statements.
  3. If a class is only used within its own package, make it package-private.
  4. If a class is used by exactly one external package, consider whether that dependency should exist. If not, refactor to remove it, then make the class package-private.
  5. Document the remaining public classes as the package’s public API.

In the logistics codebase, this audit reduced the number of public classes from 338 to 94. The remaining 244 classes became package-private. The number of possible inter-package dependencies dropped by 72%.

The Rule

For every package, identify the 2 to 4 classes that constitute its public API and make everything else package-private. If the IDE stops suggesting a class in autocomplete for developers outside the package, the boundary is working. If a class needs to be public for testing, move the test to the same package (Java allows tests in the same package to access package-private classes) rather than making the class public. The default visibility for a new class should be package-private. Make it public only when another package has a legitimate reason to depend on it.