ArchitectMind
  1. Home
  2. Tutorials
  3. Book 5 · Software Design & Design Patterns
  4. Volume VII
  5. Class-Level Design
Book 5 · Volume VII — Advanced Design & Architecture-Level Design Decisions · Topic 1.2

Class-Level Design

Cohesion, encapsulation and collaboration

9 min readStage 2 · Frameworks & design8 interview questions
Chapter
Chapter 1 — Design at Multiple Scales
Prerequisites
Topic 1.1; Volume II (OOD & SOLID); Volume VI Topics 3.2, 6.3
You will be able to
Design classes around a single cohesive responsibility, encapsulate state so invariants cannot be broken, choose between entities, value objects, services and policies, design collaborations with clear message flows, and apply GRASP heuristics to assign responsibilities
Covers
Cohesion; Encapsulation; Collaboration
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — Assigning Responsibilities (GRASP)
  5. 4A. Deep Dive — Entities, Value Objects and Services in Modern Java
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

Class-level design decides which state and behaviour belong together in a class, how that state is protected, and how the class collaborates with others to realise use cases.

ConcernQuestionGood outcome
CohesionDo all members serve one purpose?Methods use shared state towards one responsibility
EncapsulationCan clients break the object's rules?State private; invariants enforced by behaviour
CollaborationHow do objects work together?Few, clear messages; each object does its own part
Table 1 — Three concerns of class design

2Why It Exists

Object orientation promised to put data and behaviour together; in practice many Java codebases separated them again — data classes with getters/setters, and "service" classes holding logic (the anemic domain model). The result is scattered rules, duplicated validation and objects that can be put into invalid states by any caller. Class-level design restores the original idea: objects that are responsible for their own consistency.

3Core Concepts

3.1 Cohesion

Class cohesion
Figure 1 — Class cohesion

A class is cohesive when its methods work on its fields toward one responsibility (Volume VI, 6.3 for metrics).

Class roleCohesion looks likeJava form
EntityIdentity + lifecycle behaviour over its stateOrder with addLine, pay, cancel
Value objectImmutable value + operations on itrecord Money(BigDecimal, Currency) with plus
Domain service / policyOne business decision spanning objectsDiscountPolicy.discountFor(order, customer)
Application serviceOne use case orchestrationPlaceOrderService
AdapterOne technology concernJpaOrderRepository, RazorpayGateway
Table 2 — Cohesive class roles

Cohesion red flags: name suffixes like Utils/Manager, members used by disjoint method groups, many unrelated injected dependencies, frequent changes for unrelated reasons.

3.2 Encapsulation

Encapsulation: protect invariants behind behaviour
Figure 2 — Encapsulation: protect invariants behind behaviour

Encapsulation means clients cannot see or modify internal state directly and can only use operations that preserve the object's invariants.

JAVA
// Poor encapsulation: any caller can break the rules
public class Order {
    private List<OrderLine> lines = new ArrayList<>();
    private OrderStatus status;
    private BigDecimal total;
    public List<OrderLine> getLines() { return lines; }          // mutable list escapes
    public void setStatus(OrderStatus s) { status = s; }  // PAID → DRAFT possible
    // total can disagree with lines
    public void setTotal(BigDecimal t) {
        total = t;
    }
}
JAVA
// Encapsulated: invariants enforced, state changes only through behaviour
public class Order {
    private final OrderId id;
    private final List<OrderLine> lines = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;
 
    public void addLine(ProductId product, int quantity, Money unitPrice) {
        requireStatus(OrderStatus.DRAFT, "add lines");
        if (quantity <= 0) {
            throw new IllegalArgumentException("quantity must be positive");
        }
        lines.add(new OrderLine(product, quantity, unitPrice));
    }
 
    public void pay(PaymentReference ref) {
        requireStatus(OrderStatus.DRAFT, "pay");
        if (lines.isEmpty()) throw new EmptyOrder(id);
        status = OrderStatus.PAID;
    }
 
    public Money total() {  // derived, never stored inconsistently
        return lines.stream()
                    .map(OrderLine::lineTotal)
                    .reduce(Money.ZERO_INR, Money::plus);
    }
 
    public List<OrderLine> lines() { return List.copyOf(lines); }  // read-only snapshot
    public OrderStatus status() { return status; }
 
    private void requireStatus(OrderStatus expected, String action) {
        if (status != expected) throw new IllegalOrderState(id, status, action);
    }
}
Encapsulation techniqueJava means
Hide fieldsprivate fields; no setters by default
Immutable valuesRecords, final fields, List.copyOf
Validate at constructionCompact constructors, factory methods
Expose behaviour, not dataorder.pay(…) instead of setStatus(PAID)
Prevent subclass tamperingfinal classes, sealed hierarchies
Package-private collaboratorsHide helper classes inside the package
Table 3 — Encapsulation toolbox

3.3 Collaboration

Collaboration: objects sending messages
Figure 3 — Collaboration: objects sending messages

Collaboration design decides who calls whom with what message. Good collaborations:

GuidelineMeaning
Tell, don't askSend commands to the object that owns the data rather than querying and deciding elsewhere
Law of DemeterTalk to direct collaborators, not to their internals (a.getB().getC())
Few collaborators per classHigh fan-out indicates too many responsibilities (Volume VI, 6.2)
Depend on abstractions at boundariesPorts/interfaces for infrastructure collaborators
Explicit dependenciesConstructor injection; no hidden new or static access
Table 4 — Collaboration guidelines
JAVA
// Collaboration through constructor-injected roles; each collaborator has one job
@Service
public class PlaceOrderService {
    private final Carts carts;
    private final PricingPolicy pricing;
    private final Orders orders;
    private final DomainEvents events;
 
    public PlaceOrderService(Carts carts, PricingPolicy pricing,
                             Orders orders, DomainEvents events) {
        this.carts = carts; this.pricing = pricing;
        this.orders = orders; this.events = events;
    }
 
    @Transactional
    public OrderId place(CartId cartId, CustomerId customer) {
        Cart cart = carts.require(cartId);
        PricedCart priced = pricing.price(cart, customer);  // ask the expert to decide
        Order order = Order.fromPricedCart(customer, priced);      // creator creates
        orders.save(order);
        events.publish(new OrderPlaced(order.id(), order.total()));
        return order.id();
    }
}

4How It Works Internally — Assigning Responsibilities (GRASP)

GRASP heuristics for class responsibilities
Figure 4 — GRASP heuristics for class responsibilities
StepQuestionHeuristic
1Which data does the behaviour need?Information Expert — put it with that data
2Who should create this object?Creator — the aggregate that contains or closely uses it
3Does the behaviour span several objects?Domain service or policy (Pure Fabrication)
4Will this behaviour vary?Protected Variations — interface + strategies
5Who handles the use-case request?Controller — application service
6Does the assignment raise coupling?Low Coupling — reconsider
Table 5 — Responsibility assignment procedure

4ADeep Dive — Entities, Value Objects and Services in Modern Java

TypeIdentityMutabilityEqualityJava 25 form
Value objectNoneImmutableBy valuerecord Money(...), record Email(...)
EntityStable idControlled mutation via behaviourBy idClass with private state; JPA @Entity optional
Aggregate rootId; consistency boundaryMutation only via rootBy idOrder controlling OrderLines
Domain serviceNoneStateless—final class or @Component without state
Application serviceNoneStateless—@Service orchestrating use cases
Table 6 — Class roles in domain-driven design
JAVA
// Value object: validation + behaviour, immutable, equality by value
public record Money(BigDecimal amount, Currency currency) implements Comparable<Money> {
    public static final Money ZERO_INR =
            new Money(BigDecimal.ZERO, Currency.getInstance("INR"));
    public Money {
        Objects.requireNonNull(currency);
        amount = amount.setScale(currency.getDefaultFractionDigits(),
                                 RoundingMode.HALF_EVEN);
    }
    public Money plus(Money other) {
        requireSame(other);
        return new Money(amount.add(other.amount), currency);
    }
    public Money times(long n) {
        return new Money(amount.multiply(BigDecimal.valueOf(n)), currency);
    }
    public int compareTo(Money o) { requireSame(o); return amount.compareTo(o.amount); }
    private void requireSame(Money o) {
        if (!currency.equals(o.currency)) {
            throw new CurrencyMismatch(currency, o.currency);
        }
    }
}

JPA entities and encapsulation: JPA needs a no-arg constructor and field access — use protected constructors, field access (@Access(FIELD)), no public setters, and @Embeddable records for value objects. Keep collections private; expose copies.

5APIs / Features / Syntax

FeatureClass design use
Records, compact constructorsValue objects with validation
Sealed classes/interfacesControlled variants of a concept
final classes (default stance)Prevent unintended inheritance
List.copyOf, Collections.unmodifiableListEncapsulate collections
Constructor injection (Spring)Explicit collaborators
Package-private visibilityHide helper classes
ArchUnitRules like "no public setters on domain entities"
Table 7 — Language and framework support
JAVA
@ArchTest
static final ArchRule noSettersOnDomainEntities =
    noMethods().that().areDeclaredInClassesThat().resideInAPackage("..domain..")
               .and().haveNameMatching("set[A-Z].*")
               .should().bePublic();

6Practical Examples

6.1 Moving validation into value objects

Email, phone and PAN validation spread over controllers and services moved into records with compact constructors; invalid values can no longer exist past the boundary.

6.2 Replacing a Utils class

OrderUtils.calculateTotal(order), OrderUtils.isCancellable(order) and OrderUtils.canShip(order) moved into Order (Information Expert); OrderUtils was deleted.

6.3 Worked example — from anemic to rich class design

AspectAnemic Subscription + SubscriptionServiceRich Subscription
State changessetStatus, setPlan, setRenewalDate from serviceupgradeTo(plan, clock), cancel(reason, clock), renew(clock)
RulesIn service methods, duplicated in batch jobIn the entity, once
Invalid states possibleYes (cancelled + auto-renew)No — transitions checked
TestsService tests with mocksPlain entity tests
Table 8 — Refactoring outcome
JAVA
public void upgradeTo(Plan newPlan, Clock clock) {
    if (status != SubscriptionStatus.ACTIVE) {
        throw new IllegalSubscriptionState(id, status, "upgrade");
    }
    if (!newPlan.isHigherThan(plan)) throw new NotAnUpgrade(plan, newPlan);
    plan = newPlan;
    changes.add(new PlanChanged(id, plan, Instant.now(clock)));
}

6.4 Worked example — designing collaborations for a use case

Use case "refund an order line":

ResponsibilityClassHeuristic
Receive the request, transaction boundaryRefundService (application)Controller
Decide refundable amountRefundPolicy (domain service)Pure Fabrication; varies by product type
Record the refund against the lineOrder.refundLine(lineId, amount)Information Expert
Execute money movementPaymentGateway portProtected Variations
Notify customerDomainEvents → notifierLow Coupling (event)
Table 9 — Collaboration design
JAVA
@Transactional
public RefundId refund(OrderId orderId, LineId lineId, RefundReason reason) {
    Order order = orders.require(orderId);
    Money amount = refundPolicy.refundableAmount(order, lineId, reason, clock);
    Refund refund = order.refundLine(lineId, amount, reason);
    paymentGateway.refund(order.paymentReference(), amount);
    events.publish(new LineRefunded(orderId, lineId, amount));
    return refund.id();
}

6.5 Worked example — class design review

CheckFinding in CustomerService (900 lines)Action
Cohesion (dependency clusters)3 clusters: profile, addresses, notificationsExtract AddressBook, CustomerNotifier
EncapsulationCustomer has 22 public settersReplace with behaviour methods
CollaborationService navigates customer.getAccount().getTier().getDiscount()Hide delegate: customer.discountRate()
RolesValidation logic in serviceMove into Email, PhoneNumber records
Table 10 — Review findings

7Common Problems & Mistakes

  • Anemic domain models with public setters and logic in services.
  • Leaking mutable internals (collections, arrays, dates).
  • God classes with many unrelated collaborators.
  • Deep navigation chains instead of asking the expert.
  • Inheritance for code reuse instead of composition.
  • Framework annotations everywhere obscuring the domain model.

8Performance & Production Considerations

AspectNote
Immutable value objectsThread-safe; cheap allocation; safe to cache
Defensive copiesSmall cost; use unmodifiable views for large collections
Rich JPA entitiesWatch lazy loading inside domain methods; fetch needed associations
Stateless servicesSafe as Spring singletons; scale horizontally
Table 11 — Runtime considerations

Small, well-encapsulated classes rarely cost measurable performance on a modern JVM. Short-lived value objects such as Money are allocated in thread-local buffers and collected cheaply in the young generation, and escape analysis can remove many allocations entirely when an object never leaves the method that created it. Immutable records are also safe to share across threads and to cache without locks. The real production risks lie elsewhere: rich JPA entities whose behaviour methods touch lazy associations cause N+1 queries, so fetch what each use case needs with fetch joins or entity graphs. Defensive copies of large collections on hot paths add garbage; return unmodifiable views there. Profile with JFR before trading encapsulation for speed.

9Architecture & Design Considerations

  • Treat classes as self-protecting units; the same principle defines module and service APIs.
  • Use value objects as the shared vocabulary across modules.
  • Keep application services thin and domain objects rich.
  • Enforce class-level rules (no setters, no framework in domain) with architecture tests.

10Interview Questions & Answers

Q1BasicConcept

What is class cohesion?

Class cohesion is the degree to which a class's members work together towards one responsibility. In a cohesive class most methods use most of the fields, and every member serves the same purpose, such as Order managing its lines and lifecycle, or a Money record holding a value and its arithmetic. Low cohesion shows up as Utils or Manager names, groups of methods that use disjoint subsets of fields, many unrelated injected dependencies, and a class that changes for unrelated reasons. Metrics such as LCOM quantify this. High cohesion matters because a requirement then maps to one place, and the class is easier to name, understand, test and change safely.

Q2BasicConcept

What is encapsulation?

Encapsulation means an object hides its internal state and lets clients change it only through operations that preserve its invariants. It is more than private fields with getters and setters: a public setStatus, or a getter returning the internal mutable list, is still a hole. A well-encapsulated Order keeps its lines private, exposes addLine and pay methods that check the current status, derives its total instead of storing it, and returns List.copyOf(lines) so callers cannot modify internals. As a result, invalid states such as a paid order with no lines cannot be created by any caller, and each rule lives in one place where it cannot drift.

Q3IntermediateTricky

Why are public setters on entities a problem?

Public setters let any caller put the entity into an invalid state, and they push business rules out of the entity into every caller. With setStatus available, code can move a PAID order back to DRAFT or mark an empty order as paid; with setTotal, the stored total can disagree with the lines. Every service, batch job and controller then has to remember the rules, so they get duplicated, drift apart and are tested separately, which is the anemic domain model. Setters also hide intent: setStatus(CANCELLED) says less than cancel(reason, clock). Replace setters with behaviour methods that validate transitions, and create valid objects through constructors or factories.

Q4IntermediateConcept

What is Tell, Don't Ask?

Tell, Don't Ask means sending a command to the object that owns the data instead of querying its state and making the decision elsewhere. The ask style reads account.getBalance(), compares it with the amount, checks getStatus() and then calls setBalance() from a service. The tell style calls account.withdraw(amount) and lets Account enforce the balance and frozen-account rules itself, throwing a domain exception when needed. Telling keeps behaviour next to its data, which is Information Expert, keeps invariants in one place and reduces coupling to internal structure. It works hand in hand with the Law of Demeter. Queries are still fine for display and reporting; the rule targets decisions and state changes.

Q5Senior EngineerConcept

How do value objects differ from entities?

A value object has no identity: it is defined entirely by its attributes, is immutable and is compared by value, so two Money instances of 100 INR are interchangeable. An entity has a stable identity that persists while its attributes change over a lifecycle, so two customers with the same name are still different customers, and equality is by ID. In Java 25, value objects are naturally records with compact constructors for validation, such as Money or Email, while entities are classes with private state changed only through behaviour methods. Prefer value objects wherever identity is not needed: they are thread-safe, easy to test, safe to share and cure primitive obsession.

Q6Senior EngineerDesign

How do you assign a responsibility to a class?

I use the GRASP heuristics as a sequence of questions. Which data does the behaviour need? Information Expert puts it in the class holding that data. Who creates this object? Creator suggests the aggregate that contains or closely uses it, so Order creates its OrderLines. Does the behaviour span several objects and fit none of them? Make a domain service or policy, a Pure Fabrication such as RefundPolicy. Will it vary? Protected Variations puts it behind an interface with strategies. Who receives the use-case request? An application service acting as Controller. Finally I check Low Coupling and High Cohesion: if an assignment adds many dependencies or unrelated members, I reconsider it.

Q7Technical ArchitectCode

How do you keep JPA entities well encapsulated?

JPA needs a no-argument constructor and a way to set fields, but not public setters. Use field access, by annotating fields or with @Access(AccessType.FIELD), so Hibernate writes fields directly and the class can expose only behaviour methods. Make the no-arg constructor protected for the provider, and offer a public constructor or static factory that creates a valid entity. Keep collections private, mutate them only through methods such as addLine, and return List.copyOf or an unmodifiable view. Model values as @Embeddable types, which Hibernate 6.2 and later also accept as records, for example Money. Ban public setters with an ArchUnit rule, and watch lazy loading inside domain methods.

Q8Solution ArchitectDesign

How does class-level design relate to service design?

The same principles apply at every scale; only the boundary gets larger. A cohesive class owns one responsibility; a well-designed service owns one business capability and changes for one reason. Encapsulation becomes data ownership: a service hides its database behind its API just as a class hides its fields behind methods, and no other service reads its tables. Intention-revealing methods become small APIs with commands like PlaceOrder rather than generic CRUD endpoints. Collaboration rules such as Tell, Don't Ask and few collaborators become avoiding chatty calls and long synchronous chains, often by using events. Teams that design self-protecting classes tend to build self-protecting modules and services.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 5

Software Design & Design Patterns

Volume I · Topic 1.1

Software Design

Software Design Foundations & Principles

Volume I · Topic 1.2

Why Software Design Matters

Software Design Foundations & Principles

Volume I · Topic 1.3

Good vs Poor Design

Software Design Foundations & Principles

Volume II · Topic 1.1

Objects

Object-Oriented Design & SOLID

Volume II · Topic 1.2

Classes

Object-Oriented Design & SOLID

Volume II · Topic 1.3

Relationships

Object-Oriented Design & SOLID

Volume III · Topic 1.1

What Is a Design Pattern?

Creational & Structural Design Patterns

Volume III · Topic 1.2

Pattern Categories

Creational & Structural Design Patterns

Volume III · Topic 1.3

Pattern vs Framework

Creational & Structural Design Patterns

Volume IV · Topic 1.1

Strategy Pattern

Behavioral Design Patterns

Volume IV · Topic 1.2

Strategy in Java

Behavioral Design Patterns

Volume IV · Topic 1.3

Strategy vs Conditional Logic

Behavioral Design Patterns

Volume V · Topic 1.1

Layered Architecture

Enterprise & Application Design Patterns

Volume V · Topic 1.2

Service Layer

Enterprise & Application Design Patterns

Volume V · Topic 1.3

Repository

Enterprise & Application Design Patterns

Volume VI · Topic 1.1

What Is Refactoring?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.2

Why Refactor?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.3

Refactoring Safety

Refactoring, Code Quality & Evolutionary Design

Volume VII · Topic 1.1

Method-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.3

Module-Level Design

Advanced Design & Architecture-Level Design Decisions