Capa do artigo: Low Coupling and High Cohesion: The Dance of Dependencies
Series Best Practices with GRASP Β· Part 2

Low Coupling and High Cohesion: The Dance of Dependencies

Series Best Practices with GRASP Part 2

Reconnecting with the Hives

In the previous chapter, we explored Information Expert and Creator, the principles that determine where to place knowledge and who should create objects. We saw how our urban hive knows how to analyze its own vibration patterns and how it naturally creates alerts when it detects risks. Responsibilities found their natural owners. Now the challenge is to make those responsibilities collaborate without becoming dependent on each other.

But isolated responsibilities do not form systems. A hive does not function because each bee is an expert in its individual task. It functions because there is an emergent organization, a network of minimal but sufficient dependencies. Worker bees collect pollen without needing to know the details of honey production. Nurse bees feed larvae without needing to know flight routes. Each one does its work, connecting with others only when necessary.

In code, this is the essence of Low Coupling and High Cohesion: well-defined responsibilities that collaborate through minimal interfaces, where each component is strongly cohesive internally but loosely coupled externally.

Low Coupling: The Art of Not Getting Tied Down

Low Coupling is perhaps the most preached and least understood principle. Everyone knows that coupling is bad, but few understand that zero coupling is impossible and, in some cases, undesirable. The goal is not to eliminate coupling, but to control it and direct it to the right places.

The Illusion of Zero Coupling and the Reality of Metaphysics

There is a tendency in software development to seek ever higher abstractions, layer upon layer of indirection, all in the name of "reducing coupling." This pursuit of zero coupling reminds me of the philosophical journey of Socrates and Plato toward metaphysics.

Plato proposed that the physical world we perceive is merely an imperfect shadow of a world of ideal, abstract, perfect forms. A specific chair you see is an imperfect instance of the ideal Form of "chairness." This growing abstraction led to metaphysics: the study of realities beyond the physical, increasingly distant from the concrete and tangible.

In code, I see developers creating similar layers of abstraction. A concrete class is "impure," so we create an interface. But the interface is still "coupled" to a specific domain, so we create a more generic interface. And we keep climbing toward platonic abstractions: IEntity, IService, IProcessor, IHandler. Eventually, we reach abstractions so generic they have lost all concrete meaning.

The problem is the same one later philosophers identified in Platonic metaphysics: when you move too far from the concrete in pursuit of pure abstraction, you lose the ability to talk about real things. An IProcessor can be anything that processes something. But what does it process? How? Why? Abstraction, taken to the extreme, makes code incomprehensible to anyone who lacks the depth to navigate those layers.

// EXCESSIVE ABSTRACTION - metaphysics in code
interface IProcessor<TInput, TOutput, TContext> {
    process(input: TInput, context: TContext): Promise<TOutput>;
}

interface IHandler<TRequest, TResponse> {
    handle(request: TRequest): Promise<TResponse>;
}

interface IExecutor<TCommand, TResult> {
    execute(command: TCommand): Promise<TResult>;
}

// Three interfaces that are essentially the same thing
// but so abstract they communicate nothing about the domain

Compare with abstractions that maintain domain meaning:

// ABSTRACTION WITH DOMAIN MEANING
interface HiveHealthAssessor {
    assess(hive: BeeHive): Promise<HiveHealthReport>;
}

interface SwarmRiskDetector {
    detectRisk(vibrationData: VibrationMeasurement[]): SwarmRiskLevel;
}

interface AlertNotifier {
    notify(recipient: Beekeeper, alert: Alert): Promise<void>;
}

// Each interface expresses a specific domain concept
// maintaining flexibility without losing meaning

The lesson here is crucial: Low Coupling does not mean abstracting everything until meaning is lost. It means decoupling from what is volatile and unstable, while remaining coupled to what is stable and essential. The domain is stable. Business rules are essential. Infrastructure, frameworks, vendors are volatile.

Aristotle, Plato's student, criticized the theory of forms precisely for this problem. He argued that we should study concrete, real things, not only their ideal abstractions. In code, this translates to keeping abstractions close to the concrete domain. Do not create a generic IProcessor, create a specific HiveHealthAssessor.

Inherent vs. Accidental Coupling

Not all coupling is equal. There is inherent coupling (essential, inevitable, desirable) and accidental coupling (unnecessary, problematic).

Inherent coupling occurs when two things are fundamentally related in the domain. A hive is inherently coupled to its geographic location. There is no point in decoupling this with a massive abstraction layer. The hive needs a location to exist.

class UrbanBeeHive {
    constructor(
        private readonly id: HiveId,
        private readonly location: RooftopLocation  // Inherent and correct coupling
    ) {}

    // The hive MUST know its location
    isInCriticalZone(): boolean {
        return this.location.isInUrbanCenter();
    }
}

Accidental coupling occurs when a dependency exists due to implementation choices, not domain necessity. A hive should not know about JSON serialization format, which database persists its data, or which map API represents its location.

// ACCIDENTAL COUPLING - AVOID
class UrbanBeeHive {
    constructor(
        private readonly googleMapsAPI: GoogleMapsAPI  // Accidental!
    ) {}

    getLocationCoordinates(): {lat: number, lng: number} {
        // Hive should not know Google Maps API
        return this.googleMapsAPI.geocode(this.address);
    }
}

// CORRECT - coupling to the concept, not the implementation
class UrbanBeeHive {
    constructor(
        private readonly location: RooftopLocation  // Domain concept
    ) {}

    getCoordinates(): GeographicCoordinates {
        return this.location.coordinates;
    }
}

Low Coupling means eliminating accidental coupling while accepting inherent coupling. This distinction requires judgment and domain knowledge. There is no mechanical rule that replaces deep understanding.

Multiple Paths to Low Coupling

Domain events are one approach to decoupling the hive from notification services, and they will appear in the examples ahead. But they are not the only solution. There are multiple patterns for achieving low coupling, each suited to different contexts.

Observer Pattern: Direct Observers

The Observer pattern establishes a one-to-many relationship where a subject notifies multiple observers when its state changes.

interface HiveStateObserver {
    onHiveStateChanged(hive: BeeHive, change: StateChange): void;
}

class BeeHive {
    private observers: HiveStateObserver[] = [];

    subscribe(observer: HiveStateObserver): void {
        this.observers.push(observer);
    }

    unsubscribe(observer: HiveStateObserver): void {
        this.observers = this.observers.filter(o => o !== observer);
    }

    private notifyStateChange(change: StateChange): void {
        this.observers.forEach(o => o.onHiveStateChanged(this, change));
    }

    updateTemperature(temp: Temperature): void {
        const oldTemp = this.temperature;
        this.temperature = temp;
        this.notifyStateChange(new TemperatureChanged(oldTemp, temp));
    }
}

class TemperatureMonitor implements HiveStateObserver {
    onHiveStateChanged(hive: BeeHive, change: StateChange): void {
        if (change instanceof TemperatureChanged) {
            if (change.newTemp.isCritical()) {
                this.alertService.sendCriticalTemperatureAlert(hive.id, change.newTemp);
            }
        }
    }
}

Observer is straightforward and simple, but creates a subtle coupling: the subject (BeeHive) holds references to observers, which can cause lifecycle issues and memory leaks if not managed carefully.

Event Bus: Full Mediation

An Event Bus centralizes event publishing and subscription, completely removing the need for the publisher to know subscribers.

class DomainEventBus {
    private handlers = new Map<string, Array<(event: any) => void>>();

    subscribe<T extends DomainEvent>(
        eventType: string,
        handler: (event: T) => void
    ): void {
        if (!this.handlers.has(eventType)) {
            this.handlers.set(eventType, []);
        }
        this.handlers.get(eventType)!.push(handler);
    }

    publish(event: DomainEvent): void {
        const handlers = this.handlers.get(event.type) || [];
        handlers.forEach(handler => handler(event));
    }
}

class BeeHive {
    constructor(
        private readonly eventBus: DomainEventBus
    ) {}

    updateTemperature(temp: Temperature): void {
        const oldTemp = this.temperature;
        this.temperature = temp;

        // Publishes without knowing who will consume
        this.eventBus.publish(new TemperatureChangedEvent(
            this.id,
            oldTemp,
            temp
        ));
    }
}

// Somewhere in the application layer
eventBus.subscribe('TemperatureChanged', (event: TemperatureChangedEvent) => {
    if (event.newTemp.isCritical()) {
        alertService.sendCriticalTemperatureAlert(event.hiveId, event.newTemp);
    }
});

Event Bus completely decouples publisher from subscribers, but adds indirection and can make control flow hard to trace. It is powerful for systems where events genuinely need to cross module or context boundaries.

Dependency Injection: Inverting Control

Dependency Injection inverts who controls dependencies: instead of a class instantiating its dependencies, they are injected from outside.

// WITHOUT DI - high coupling
class HiveMonitoringService {
    private readonly repository = new PostgresHiveRepository();  // Coupled!
    private readonly notifier = new SendGridEmailNotifier();    // Coupled!

    async checkHive(hiveId: HiveId): Promise<void> {
        const hive = await this.repository.findById(hiveId);
        // ...
    }
}

// WITH DI - low coupling
class HiveMonitoringService {
    constructor(
        private readonly repository: HiveRepository,      // Interface!
        private readonly notifier: AlertNotifier          // Interface!
    ) {}

    async checkHive(hiveId: HiveId): Promise<void> {
        const hive = await this.repository.findById(hiveId);
        // ...
    }
}

// Composition happens at the composition root
const service = new HiveMonitoringService(
    new PostgresHiveRepository(dbConnection),
    new MultiChannelNotifier(emailService, smsService)
);

DI is fundamental for testability and for applying the Dependency Inversion Principle (DIP). It allows swapping implementations without touching the code that uses them.

Strategy Pattern: Interchangeable Behaviors

Strategy encapsulates interchangeable algorithms, allowing the client to choose which to use without knowing implementation details.

interface SwarmDetectionStrategy {
    detectRisk(pattern: VibrationPattern): SwarmRiskLevel;
}

class FrequencyThresholdStrategy implements SwarmDetectionStrategy {
    detectRisk(pattern: VibrationPattern): SwarmRiskLevel {
        // Algorithm based on simple threshold
    }
}

class MachineLearningStrategy implements SwarmDetectionStrategy {
    detectRisk(pattern: VibrationPattern): SwarmRiskLevel {
        // ML-based algorithm
    }
}

class SwarmMonitor {
    constructor(
        private readonly strategy: SwarmDetectionStrategy  // Decoupled from the specific algorithm
    ) {}

    assessSwarmRisk(hive: BeeHive): SwarmRiskLevel {
        const pattern = hive.getVibrationPattern();
        return this.strategy.detectRisk(pattern);  // Delegates to the strategy
    }
}

// Flexibility to swap strategies
const basicMonitor = new SwarmMonitor(new FrequencyThresholdStrategy());
const advancedMonitor = new SwarmMonitor(new MachineLearningStrategy());

Strategy is Low Coupling through polymorphism. The monitor is not coupled to a specific algorithm, it is coupled to the abstract concept of "detection strategy".

Observer works well when the subject needs to notify observers within the same process and their lifecycle is controlled. Event Bus is more appropriate when events need to cross module boundaries or when publisher and subscriber must not know each other at all. DI solves the problem of infrastructure dependencies and is indispensable for testability. Strategy encapsulates behavioral variation when an algorithm needs to be interchangeable without changing who uses it. These patterns are not mutually exclusive. In a real system, all four coexist in different layers. The choice of each depends on the nature of the coupling that needs to be reduced.

The Wisdom of Conscious Coupling

The art of Low Coupling is not in eliminating every dependency, but in becoming conscious of each one. The first question facing any dependency is whether it is inherent or accidental. Inherent coupling should be accepted. Accidental coupling should be removed.

The second question is whether the dependency points to something stable or something volatile. Depending on an implementation that changes for reasons external to the domain is a guaranteed source of instability. The answer is to create indirection through stable abstractions, typically interfaces that express domain concepts rather than technical details. When a dependency crosses architectural boundaries, this protection stops being optional.

A practical way to detect excessive coupling is to check whether the code can be tested without that dependency. If it cannot, Dependency Injection solves the problem by inverting control. The test is not just a safety net, it is a coupling detector.

"Make sure that the modules that make up your application's business logic are not coupled to the outside world."
β€” Robert C. Martin, Clean Architecture

Low Coupling is balance. It is architectural wisdom, not mechanical rule application. It is Aristotle, not Plato. Grounded in the concrete, conscious of necessary abstractions.

High Cohesion: When Everything Connects in the Right Place

If Low Coupling governs how components relate to the outside world, High Cohesion governs what can coexist within a component. A cohesive class is one where everything inside has a related reason to exist together, where every method, every attribute, every decision points to the same purpose.

In sensor data processing, the temptation is to create a DataProcessor class that processes vibration, temperature, humidity, and weight, and also performs malformed data validation and unit conversion. Technically it works, but the class has low cohesion. The elements are together simply because they are data processing, not because they share a real purpose.

// LOW COHESION - avoid
class SensorDataProcessor {
    processVibration(raw: number): VibrationMeasurement { /* ... */ }
    processTemperature(raw: number): Temperature { /* ... */ }
    processHumidity(raw: number): Humidity { /* ... */ }
    processWeight(raw: number): Weight { /* ... */ }
    validateDataFormat(raw: string): boolean { /* ... */ }
    convertCelsiusToFahrenheit(c: number): number { /* ... */ }
    convertKilogramsToPounds(kg: number): number { /* ... */ }
    detectAnomalies(data: any[]): Anomaly[] { /* ... */ }
}

This class has multiple reasons to change: changes in how to process vibration, temperature, validation format, units of measure, anomaly detection. That is low cohesion and violates SRP simultaneously.

High Cohesion suggests separating by cohesive purpose:

class VibrationDataProcessor {
    private readonly calibration: SensorCalibration;

    process(raw: RawVibrationData): VibrationMeasurement {
        const calibrated = this.applyCalibration(raw);
        const validated = this.ensureWithinPhysicalBounds(calibrated);
        return VibrationMeasurement.fromHertz(validated.frequency);
    }

    private applyCalibration(raw: RawVibrationData): CalibratedData {
        return this.calibration.apply(raw);
    }

    private ensureWithinPhysicalBounds(data: CalibratedData): ValidatedData {
        if (data.frequency < 0 || data.frequency > 2000) {
            throw new SensorDataOutOfBoundsError(data);
        }
        return ValidatedData.from(data);
    }
}

class TemperatureDataProcessor {
    private readonly calibration: SensorCalibration;
    private readonly unit: TemperatureUnit;

    process(raw: RawTemperatureData): Temperature {
        const calibrated = this.applyCalibration(raw);
        const validated = this.ensureWithinPhysicalBounds(calibrated);
        return Temperature.create(validated.degrees, this.unit);
    }

    // Methods related only to temperature processing
}

Now each processor has high cohesion: everything inside it is about processing one specific type of sensor data. The reasons for change are related. This also follows KISS (Keep It Simple, Stupid): each class does one thing in a simple and understandable way.

DRY vs. Cohesion: When Duplication Is More Honest

The provocation here is that we often create premature abstractions trying to reuse code between different processors, when in fact they only share a similar form, not a common essence. DRY (Don't Repeat Yourself) does not mean eliminating all visual duplication, it means eliminating knowledge duplication. If vibration and temperature processing look similar in form but are different in essence, duplicating is more honest than forcing abstraction.

// FORCED ABSTRACTION - low cohesion by trying to avoid duplication
class GenericSensorProcessor<T> {
    process(raw: number, converter: (n: number) => T): T {
        const validated = this.validate(raw);
        return converter(validated);
    }

    private validate(raw: number): number {
        // Generic validation that doesn't actually apply to all sensors
    }
}

// Confusing usage that doesn't express domain
const vibration = genericProcessor.process(rawData, (n) => VibrationMeasurement.fromHertz(n));
// HONEST DUPLICATION - high cohesion in each processor
class VibrationDataProcessor {
    process(raw: RawVibrationData): VibrationMeasurement {
        this.validateVibrationRange(raw);  // Specific validation
        return VibrationMeasurement.fromHertz(raw.frequency);
    }

    private validateVibrationRange(raw: RawVibrationData): void {
        if (raw.frequency < 0 || raw.frequency > 2000) {
            throw new InvalidVibrationDataError(raw);
        }
    }
}

class TemperatureDataProcessor {
    process(raw: RawTemperatureData): Temperature {
        this.validateTemperatureRange(raw);  // Specific and different validation
        return Temperature.celsius(raw.degrees);
    }

    private validateTemperatureRange(raw: RawTemperatureData): void {
        if (raw.degrees < -273.15) {  // Absolute zero
            throw new PhysicallyImpossibleTemperatureError(raw);
        }
    }
}

Yes, there is a similar structure in form. Process, validation, conversion. But what defines DRY is not form, it is knowledge. DRY says that each piece of knowledge must have a unique representation in the system. Knowledge about physical temperature limits is different from knowledge about operational limits of a vibration sensor. Forcing both into a generic abstraction does not eliminate knowledge duplication. It creates an abstraction that belongs to neither domain, weakening the cohesion of both.

Honesty here is an engineering virtue. When two concepts are fundamentally distinct, keeping them separate is more precise than compressing them artificially. The visual duplication disappears, but the conceptual confusion remains, now invisible behind a generic.

Cohesion at Scale: Packages and Modules

High Cohesion is not limited to individual classes. It also applies to packages, modules, and larger components. A cohesive package groups classes that change for the same reasons and collaborate toward a common purpose.

Consider the organization of code into packages for our hive system:

src/
β”œβ”€β”€ domain/
β”‚   β”œβ”€β”€ hive/
β”‚   β”‚   β”œβ”€β”€ BeeHive.ts
β”‚   β”‚   β”œβ”€β”€ HiveId.ts
β”‚   β”‚   β”œβ”€β”€ HiveHealthScore.ts
β”‚   β”‚   └── SwarmVibrationPattern.ts
β”‚   β”œβ”€β”€ alert/
β”‚   β”‚   β”œβ”€β”€ Alert.ts
β”‚   β”‚   β”œβ”€β”€ AlertSeverity.ts
β”‚   β”‚   └── AlertType.ts
β”‚   └── sensor/
β”‚       β”œβ”€β”€ VibrationMeasurement.ts
β”‚       β”œβ”€β”€ Temperature.ts
β”‚       └── Humidity.ts
β”œβ”€β”€ application/
β”‚   β”œβ”€β”€ AssessHiveHealthUseCase.ts
β”‚   └── ProcessSensorDataUseCase.ts
└── infrastructure/
    β”œβ”€β”€ persistence/
    β”‚   └── PostgresHiveRepository.ts
    └── notification/
        └── MultiChannelNotifier.ts

Each package has high internal cohesion: domain/hive/ contains only concepts related to the hive, domain/alert/ only alert concepts. They collaborate, but each has a specific reason to exist and change.

Afferent and Efferent Coupling

When talking about cohesion and coupling at the package scale, two concepts become crucial: afferent coupling (Ca) and efferent coupling (Ce).

Efferent Coupling (Ce): the number of classes outside the package that classes inside the package depend on. Measures how much the package depends on others. High Ce means changes in other packages affect this one.

Afferent Coupling (Ca): the number of classes outside the package that depend on classes inside it. Measures how much others depend on this package. High Ca means changes in this package affect many others.

// Package domain/hive/
// Ca (afferent): application, infrastructure depend on it (high Ca)
// Ce (efferent): depends only on domain/sensor (low Ce)

// Package application/
// Ca: only infrastructure depends on it (medium Ca)
// Ce: depends on domain/hive, domain/alert (medium Ce)

// Package infrastructure/
// Ca: nobody depends on it (zero or very low Ca)
// Ce: depends on domain and application (high Ce)

The useful metric here is Instability = Ce / (Ca + Ce). Ranges from 0 (maximum stability, everyone depends on you, you depend on nobody) to 1 (maximum instability, you depend on everyone, nobody depends on you).

Stable Dependencies Principle: dependencies should point in the direction of stability. Unstable packages should depend on stable packages, not the other way around.

In our case:

  • domain/: Instability ~0.1 (very stable, everyone depends on it, it depends on almost nothing)
  • application/: Instability ~0.5 (moderately stable)
  • infrastructure/: Instability ~0.9 (highly unstable, not a problem, it is the boundary)

That is the correct direction: infrastructure β†’ application β†’ domain. Never the reverse.

Cyclic Dependencies: The Poison of Cohesion

One of the worst violations of cohesion and coupling are cyclic dependencies between packages. This is when package A depends on B, which depends on C, which depends on A. It forms a cycle that makes it impossible to understand, test, or modify any part in isolation.

// PROBLEMATIC CYCLE

// domain/hive/BeeHive.ts
import { AlertService } from '../../application/AlertService';

class BeeHive {
    constructor(private alertService: AlertService) {}  // Domain depending on application!
}

// application/AlertService.ts
import { BeeHive } from '../domain/hive/BeeHive';

class AlertService {
    checkHive(hive: BeeHive): void {  // Application depending on domain (ok)
        // But now we have a cycle: BeeHive β†’ AlertService β†’ BeeHive
    }
}

This cycle is toxic. To compile BeeHive, I need AlertService. To compile AlertService, I need BeeHive. They are cyclically coupled, effectively becoming a single inseparable component.

The solution is to break the cycle with dependency inversion:

// domain/hive/BeeHive.ts
import { DomainEventPublisher } from '../events/DomainEventPublisher';

class BeeHive {
    constructor(private eventPublisher: DomainEventPublisher) {}

    assessCurrentState(): void {
        // ...
        if (risk) {
            // Publishes event, doesn't call service directly
            this.eventPublisher.publish(new SwarmRiskDetected(this.id, risk));
        }
    }
}

// application/AlertService.ts
import { BeeHive } from '../domain/hive/BeeHive';
import { SwarmRiskDetected } from '../domain/events/SwarmRiskDetected';

class AlertService {
    constructor(eventPublisher: DomainEventPublisher) {
        // Subscribes to the event
        eventPublisher.subscribe('SwarmRiskDetected', (event) => {
            this.handleSwarmRisk(event);
        });
    }

    private handleSwarmRisk(event: SwarmRiskDetected): void {
        // Handles the alert without BeeHive depending on AlertService
    }
}

Now: BeeHive β†’ DomainEventPublisher ← AlertService. No cycle. BeeHive does not know AlertService, both know only the event mechanism.

Static dependency analysis tools detect cycles automatically. madge serves this purpose for TypeScript and JavaScript projects. For Java, SonarQube identifies cyclic dependencies between packages and generates coupling reports that can be integrated into the CI pipeline. Cyclic dependencies are one of the main sources of accidental complexity and need to be eliminated before they become structural.

To go deeper on this topic, there is a dedicated article on the Acyclic Dependencies Principle, covering everything from graph theory to the impact on the cost of change in systems that have grown beyond the monolith.

The Cohesion Metric: LCOM

Cohesion can also be measured. There is a formal metric called LCOM (Lack of Cohesion of Methods) that quantifies what intuition already suspects. It measures how many distinct groups of methods exist in a class, where each group accesses a different set of attributes. A hive where half the bees manage temperature and the other half handle billing is not a cohesive hive. High LCOM is the numerical indicator of that in code.

A class with high LCOM (many groups) has low cohesion: methods do not collaborate, they merely coexist in the same class without real reason. A class with low LCOM has high cohesion: methods work with the same data in collaboration.

// HIGH LCOM - low cohesion
class HiveManager {
    // Attribute group 1
    private temperature: Temperature;
    private humidity: Humidity;

    // Attribute group 2 (unrelated to group 1)
    private ownerId: string;
    private billingInfo: BillingInfo;

    // Group 1 methods
    assessEnvironment(): void {
        // Uses temperature and humidity
    }

    // Group 2 methods
    chargeMaintenance(): void {
        // Uses ownerId and billingInfo
    }

    // High LCOM: two distinct groups that shouldn't be together
}

// SEPARATE into cohesive classes
class HiveEnvironment {
    private temperature: Temperature;
    private humidity: Humidity;

    assess(): EnvironmentAssessment {
        // All methods use temperature and humidity
    }
}

class HiveOwnership {
    private ownerId: string;
    private billingInfo: BillingInfo;

    chargeMaintenance(): void {
        // All methods use ownerId and billingInfo
    }
}

LCOM is a useful metric for detecting classes that are doing too much and need to be split to increase cohesion.

The Dance of Low Coupling and High Cohesion

Low Coupling and High Cohesion are complementary forces that act in opposite directions and require balance. High cohesion within a component means that everything together changes for the same reasons. Low coupling between components means that changes in one do not propagate pressure to others. The tension point appears when separating too much fragments what should be unified, and uniting too much creates the God Object that knows and does everything.

The hive resolves this tension with biological precision. Each bee has a cohesive, specialized, unmistakable role. But it collaborates through minimal and sufficient protocols. There is no bee that does everything, and there are no bees without communication with the collective. The system works because each unit is cohesive internally and loosely coupled externally.

In code, this balance is not a state achieved once. It is a constant pressure, a judgment remade with each new dependency, with each new responsibility finding a place to live.

"A software system is well-structured if its modules are strongly cohesive and loosely coupled."
β€” Grady Booch, Object-Oriented Analysis and Design


What Comes Next

Now that we understand how to organize responsibilities (Information Expert, Creator) and how to connect them strategically (Low Coupling, High Cohesion), a practical question arises: who coordinates all of this? When multiple entities need to collaborate to fulfill a complete use case, who is the conductor?

In the next chapter, we will explore Controller and Polymorphism. We will see that Controller in GRASP is different from the Controller in MVC, and understand when each concept applies. We will discover how polymorphism is not just about inheritance, but about assigning responsibilities based on behavioral variation. And we will face a professional provocation: writing more structured code is not wasted work, it is an investment in maintainability.

The journey continues. Bees do not work at random, there is an implicit coordination. In code, that coordination must be explicit, controlled, and adaptable to change.