Private Class Data
Private Class Data isolates attributes in a write-once data object exposed read-only, enforcing immutability and shrinking the code that can mutate state. Native immutability features now largely supersede it.
The Private Class Data pattern restricts write access to a class's attributes by separating data from the methods that use it. The data is moved into a dedicated data-holder object that is initialized once at construction and thereafter exposed only through read accessors. It solves over-exposure: even "private" fields are mutable from within the owning class's many methods, and that internal write access can be a source of accidental state corruption and broken invariants.
How It Works
The class delegates its attributes to a separate data class whose fields are set exactly once, in its constructor. The data class provides getters but no setters, so once constructed its values cannot change. The main class holds a reference to this data object and reads from it, but neither external code nor the main class's own methods can mutate the protected values after initialization. The result is that the data is effectively immutable and write access is confined to the single point of construction.
This tightens encapsulation beyond ordinary private fields by removing internal write access, not just external access, and by shrinking the surface where state can change to one place.
When to Use It
Use Private Class Data when you want to guarantee that certain attributes never change after an object is built, when a class has fields that should be immutable but the language lacks first-class immutability, and when you want to minimize the code that can modify sensitive state to protect invariants. It is useful for configuration objects, value-like entities, and security-sensitive data where uncontrolled mutation would be dangerous.
It is largely unnecessary in languages with built-in immutability constructs — final/readonly fields, records, frozen objects, or value types — which achieve the same guarantee more directly.
Trade-offs
The pattern enforces immutability and encapsulation, reduces the chance of accidental state changes, and confines write access to construction, which simplifies reasoning about an object's lifetime and aids thread safety. Immutable data is inherently safe to share across threads.
The costs are extra classes and indirection for what modern languages often provide natively. It can feel like ceremony when final fields or a record would suffice, and the additional data object adds a small memory and access overhead. The pattern predates and is largely superseded by language-level immutability features, so its main value today is in environments lacking them.
Related Patterns
Private Class Data pairs naturally with Builder, which assembles the immutable data object step by step before construction. It shares immutability goals with Flyweight, whose shared objects must be immutable, and it resembles a read-only Proxy over the data. Value Object is a closely related concept emphasizing immutable, equality-by-value data.
Example
A Circle class holds radius, center, and color that must never change after creation. Instead of private fields, it delegates to a CircleData object whose constructor sets all three and exposes only getters. Circle reads its radius via data.getRadius() but has no way to alter it, and no setter exists anywhere, so every Circle is guaranteed immutable from the moment it is built. In modern Java the same intent is met simply by a record CircleData(double radius, Point center, Color color).