Capa do artigo: When Code Reflects the Domain: The Journey of GRASP and DDD Making a Difference in Practice
Series Best Practices with GRASP · Part 1

When Code Reflects the Domain: The Journey of GRASP and DDD Making a Difference in Practice

Series Best Practices with GRASP Part 1

The Problem of Responsibility and the First Experts

After so many years working in software development, it becomes relatively simple to look back, connect the dots, and recognize the transformations that have shaped software engineering and architecture. Those who started on small projects, with manual FTP deployments and no encrypted communication, watched the industry evolve into large monolithic systems with distributed teams, then into service-oriented architectures (SOA), enterprise components (EJB), and more recently into microservice ecosystems with automation pipelines, quality gates, and cloud infrastructure.

Not everyone lived through all these phases, and as time moves forward, priorities shift. Security and governance models adapt, team structures reconfigure, and new tools emerge at an ever-accelerating pace. However, while processes and technologies change constantly, the fundamental principles of engineering remain the same.

With the growing number of languages, frameworks, practices, and tools, a natural tendency arises to prioritize immediate, applicable knowledge, often at the expense of fundamentals. This abundance of options creates a kind of "eclipse": the conceptual foundations that sustain good engineering fall into the shadows, even though they are just as important, or more so, than the tools upon which solutions are built. Creating objects with variables does not make a project object-oriented, just as following a pattern mechanically does not guarantee good architecture. Mastery of tools is valuable, but it is the understanding of principles, such as those defined by GRASP, that truly distinguishes technical use from the conscious and ingenious use of object-oriented design.

This article is the first in a series dedicated to revisiting essential fundamentals of software engineering, starting with GRASP and extending to topics like DDD and other practices that help transform technical knowledge into more conscious and sustainable design decisions.

The Problem Nobody Wants to Admit

Have you ever opened a code file and felt that sensation of reading a poorly written thriller, where characters appear out of nowhere doing things that make no sense for the plot? That class that should represent a domain concept but that, mysteriously, knows how to connect to a database, send emails, validate permissions, and on top of it all calculate taxes? This is the most visible symptom of an invisible problem: the inability to distribute responsibilities.

The inconvenient truth is that most developers learn design patterns by memorizing ready-made solutions. Factory here, Strategy there, Repository over there. But what we really need to understand is not the name of the patterns, but the fundamental principle that originates them. And this principle has a name that sounds almost too academic to be taken seriously: GRASP - General Responsibility Assignment Software Patterns.

"The critical design tool for software development is a mind well educated in design principles. It is not the UML or any other technology." — Craig Larman, Applying UML and Patterns

When Responsibility Meets the Domain

Imagine you are developing a system for monitoring urban beehives. Yes, those bee boxes that people place on building rooftops to help pollinate the city. The system needs to process sensor data: temperature, humidity, hive weight, wing vibration frequency, bee entry and exit. When vibration reaches certain patterns, it may indicate that the queen is about to swarm, and the beekeeper needs to be alerted.

The first temptation is to create a BeeHiveMonitor class that does everything. And in fact, it works: the code runs, tests pass, the system goes to production. But six months later, when you need to add a new sensor type, change the alerting logic, or integrate with a new hardware vendor, the code transforms into a web of dependencies as complex as the social organization of the bees themselves.

The question is not whether the system works. I often say that, from the business side, it matters little whether you are using an architecture like Clean Architecture or whether all the code is concentrated in a single file, as long as the system operates without failures. However, in a corporate environment, where work is essentially collaborative, good software design makes all the difference. That is why I always insist: every professional software engineer actually has two deliverables. One for the business and another for themselves and their peers, the other developers. The first deliverable is the requested functionality, while the second is the construction of code with a well-structured and sustainable design.

The quality of the code you write, the care you put into structuring it clearly, and the attention you give to its readability and maintainability say more about the kind of professional you are than any title or certification.

There is a video by Brazilian professor Clóvis de Barros Filho in which he discusses Japanese ethics. He explains that, in this worldview, concern for others is essential for coexistence in society: a true art of living together.

Analogously, in software engineering, we must remember that the way we write code does not only impact our "future self," who will bear the responsibility of maintaining the system, but also the other developers who interact with our work.

Learning a programming language is just one of the developer's tools. Writing well is what transforms someone into a true software engineering professional.

DDD (Domain-Driven Design) teaches us that software should reflect the problem domain. However, reflecting does not simply mean naming classes with business nouns. It means deeply understanding the natural responsibilities of the domain and translating them into code. And this is where the principles of Responsibility and Domain truly meet.

In the context of DDD, responsibility is the key to modeling the system effectively. Each part of the code must have a clear responsibility, aligned with the domain concept being represented. This is reflected in variable names, methods, classes, and even in the structure of the system as a whole. The force that permeates and connects all these elements is the ubiquitous language, which ensures that everyone, both technical and business stakeholders, speaks the same language.

I have, in fact, another (relatively long) article about domains, which addresses how this term is frequently misunderstood. Depending on the context, its meaning can change considerably, generating noise in communication.

From this point forward, this is precisely where GRASP (General Responsibility Assignment Software Patterns) comes in. It does not compete with DDD but rather complements its application, functioning as the grammar that allows us to write correctly in the language of the domain. While DDD helps us understand what we should express in code, GRASP teaches us how to do it in a clear, responsible, and elegant way.

Information Expert: Where Information Should Reside

The first principle of GRASP is so obvious it almost feels insulting: assign responsibilities to whoever owns the information.

The Information Expert is the holder of knowledge and, therefore, should be responsible for controlling it. In another article about encapsulation, I discuss how encapsulation should be used to ensure that information and state mutation are controlled within the object. Although the Information Expert principle is not mentioned directly, it is implicitly present when applying this approach, which helps avoid anemic classes, a concept also reinforced by DDD.

But at the same time, a question arises: how can something so obvious be systematically ignored?

Let us return to the bees. You have raw sensor data: a stream of numbers representing vibrations per second. Who should know whether this pattern indicates imminent swarming? The intuitive answer is: whoever knows bee vibration patterns. In code, this translates into a domain entity.

class SwarmVibrationPattern {
    private readonly id: string;
    private readonly measurements: VibrationMeasurement[];
    private readonly threshold: FrequencyThreshold;

    constructor(
        measurements: VibrationMeasurement[],
        threshold: FrequencyThreshold
    ) {
        this.measurements = measurements;
        this.threshold = threshold;
    }

    indicatesImmediateSwarmRisk(): boolean {
        const recentWindow = this.measurements.slice(-100);
        const sustainedHighFrequency = recentWindow.filter(
            m => m.frequency.isAbove(this.threshold.critical)
        ).length;

        const accelerationRate = this.calculateAcceleration(recentWindow);

        return sustainedHighFrequency > 70 &&
               accelerationRate > this.threshold.accelerationCritical;
    }

    private calculateAcceleration(window: VibrationMeasurement[]): number {
        // Frequency acceleration analysis logic
        const derivatives = window.slice(1).map((current, idx) =>
            current.frequency.hertz - window[idx].frequency.hertz
        );
        return derivatives.reduce((sum, d) => sum + Math.abs(d), 0) / derivatives.length;
    }
}

Notice what happened here.

The class does not merely store data; it also holds the knowledge about vibration patterns, becoming the expert in determining swarm risk.

This is a clear example of Information Expert, but also of SRP (Single Responsibility Principle): the only reason for this class to change would be if the knowledge about swarming patterns changed. It is also a good application of separation of concerns: vibration analysis is separated from data collection and alert dispatching.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler, Refactoring

The challenge here is: how many times have you seen (or written) a Service that fetches data from an object only to process it externally? Or a Service bloated with rules, like a code smoothie with 1,000 lines. That is a violation of Information Expert.

It is like asking someone who speaks Russian to give you all the words of a text and then you, who do not speak Russian, try to understand the meaning. Why not let the Russian speaker interpret it?

Consider a simple but extremely common example: state validation. Imagine you have a hive maintenance task that can be in different statuses: scheduled, in progress, completed, cancelled. In poorly structured code, you frequently see this:

// INFORMATION EXPERT VIOLATION
class HiveMaintenanceService {
    async rescheduleTask(taskId: string, newDate: Date): Promise<void> {
        const task = await this.repository.findById(taskId);

        // Validation logic scattered across the service
        if (task.status === 'completed' || task.status === 'cancelled') {
            throw new Error('Cannot reschedule a completed or cancelled task');
        }

        task.scheduledDate = newDate;
        await this.repository.save(task);
    }

    async startTask(taskId: string): Promise<void> {
        const task = await this.repository.findById(taskId);

        // SAME validation logic repeated elsewhere
        if (task.status === 'completed' || task.status === 'cancelled') {
            throw new Error('Cannot start a completed or cancelled task');
        }

        task.status = 'in_progress';
        await this.repository.save(task);
    }
}

The problem here is that the knowledge about which statuses allow which transitions is scattered across the service, repeated in multiple methods. Who is the expert on a task's state? The task itself. It holds the status data and should be responsible for interpreting that status.

// APPLYING INFORMATION EXPERT
class MaintenanceTask {
    private status: TaskStatus;
    private scheduledDate: Date;

    canBeRescheduled(): boolean {
        return this.status === 'scheduled' || this.status === 'in_progress';
    }

    canBeStarted(): boolean {
        return this.status === 'scheduled';
    }

    reschedule(newDate: Date): void {
        if (!this.canBeRescheduled()) {
            throw new InvalidTaskTransitionError(
                `Task with status '${this.status}' cannot be rescheduled`
            );
        }
        this.scheduledDate = newDate;
    }

    start(): void {
        if (!this.canBeStarted()) {
            throw new InvalidTaskTransitionError(
                `Task with status '${this.status}' cannot be started`
            );
        }
        this.status = 'in_progress';
    }
}

class HiveMaintenanceService {
    async rescheduleTask(taskId: string, newDate: Date): Promise<void> {
        const task = await this.repository.findById(taskId);
        task.reschedule(newDate); // The task knows how to reschedule itself
        await this.repository.save(task);
    }

    async startTask(taskId: string): Promise<void> {
        const task = await this.repository.findById(taskId);
        task.start(); // The task knows how to start itself
        await this.repository.save(task);
    }
}

Now, the service simply orchestrates, while the entity contains both the data and the knowledge about how to operate on it. If you need to add new statuses or transition rules, you do it in a single place: in the entity itself, which is the expert on that information. The service does not need to worry about any of this. It simply delegates to the task, asking it to do what it knows how to do. This is the essence of Information Expert.

Rich Entities: The Beating Heart of the Domain

The previous example shows the difference between anemic entities and rich entities.

An anemic entity is merely a simplified data structure, with getters and setters, but without real behavior. It is like a worker bee that only carries pollen but does not know what to do with it. All the logic ends up scattered across services that manipulate that data from the outside, violating both encapsulation and the Information Expert principle simultaneously.

A rich entity, on the other hand, is a living cell of the system. It contains data and behavior that are intimately related, protecting its invariants and knowing how to transform itself. Moreover, it expresses the domain with clarity, and every state change is controlled exclusively by it.

Consider the visceral difference between these two approaches to modeling the beehive:

// ANEMIC ENTITY - avoid this
class BeeHive {
    id: string;
    temperature: number;
    humidity: number;
    weight: number;
    lastInspection: Date;

    // Only getters and setters, no behavior
    getTemperature(): number { return this.temperature; }
    setTemperature(temp: number): void { this.temperature = temp; }
    // ... more getters and setters
}

// All logic leaks into services
class BeeHiveService {
    checkIfNeedsInspection(hive: BeeHive): boolean {
        const daysSinceInspection =
            (Date.now() - hive.lastInspection.getTime()) / (1000 * 60 * 60 * 24);
        const tempInDanger = hive.temperature < 32 || hive.temperature > 36;
        const humidityInDanger = hive.humidity < 40 || hive.humidity > 70;

        return daysSinceInspection > 7 || tempInDanger || humidityInDanger;
    }

    calculateHealthScore(hive: BeeHive): number {
        // More domain logic outside the entity
    }
}
// RICH ENTITY - correct approach
class BeeHive {
    private readonly id: HiveId;
    private temperature: Temperature;
    private humidity: Humidity;
    private weight: Weight;
    private lastInspection: InspectionDate;

    // The hive KNOWS when it needs inspection
    needsInspection(currentDate: Date = new Date()): boolean {
        return this.lastInspection.hasExceededInterval(currentDate, 7) ||
               this.environmentalConditionsAreCritical();
    }

    private environmentalConditionsAreCritical(): boolean {
        return this.temperature.isOutsideIdealRange() ||
               this.humidity.isOutsideIdealRange();
    }

    // The hive KNOWS how to assess its own health
    assessHealth(): HiveHealthScore {
        const tempScore = this.temperature.healthContribution();
        const humidityScore = this.humidity.healthContribution();
        const weightScore = this.weight.healthContribution();
        const inspectionScore = this.lastInspection.healthContribution();

        return HiveHealthScore.calculate([
            tempScore,
            humidityScore,
            weightScore,
            inspectionScore
        ]);
    }

    // The hive protects its invariants
    updateTemperature(temperature: Temperature): void {
        if (temperature.isCriticallyLow()) {
            throw new CriticalTemperatureError(
              'Critical temperature requires immediate inspection before update'
            );
        }
        this.temperature = temperature;
    }
}

Notice how the rich entity does not merely store data but expresses domain concepts. The needsInspection() method is not just a boolean function; it is a direct expression of the business rule. The environmentalConditionsAreCritical() method is not an implementation detail exposed publicly, but rather private logic that contributes to a larger responsibility.

This is the essence of the rich domain model that DDD advocates: the code reads like the language of the domain expert. A beekeeper does not say "if the days since the last inspection are greater than seven." They say "the hive needs inspection." And the code should reflect exactly that.

Creator: The Birth of Entities

The Creator principle answers the question of who should be responsible for creating objects in a system. The answer is simple: whoever has, contains, or intensively uses the object to be created. By applying this principle, we avoid unnecessary scattering of creation logic and ensure that object creation happens in the most natural place.

Continuing with our bees, consider the creation of alerts. An alert does not emerge from nothing. It emerges when something analyzes data and determines there is a problem. Who is performing this analysis? The beehive, as a domain entity.

class UrbanBeeHive {
    private readonly id: HiveId;
    private readonly location: RooftopLocation;
    private readonly sensorStream: SensorDataStream;
    private vibrationHistory: SwarmVibrationPattern;

    constructor(
        id: HiveId,
        location: RooftopLocation,
        sensorStream: SensorDataStream
    ) {
        this.id = id;
        this.location = location;
        this.sensorStream = sensorStream;
        this.vibrationHistory = SwarmVibrationPattern.empty();
    }

    assessCurrentState(): HiveAssessment {
        const latestReadings = this.sensorStream.getLatestBatch();
        this.vibrationHistory = this.vibrationHistory.incorporateNew(latestReadings.vibrations);

        if (this.vibrationHistory.indicatesImmediateSwarmRisk()) {
            return this.createSwarmAlert(latestReadings);
        }

        return HiveAssessment.normal(this.id);
    }

    private createSwarmAlert(readings: SensorReadings): HiveAssessment {
        return HiveAssessment.withAlert(
            this.id,
            Alert.swarmRisk(
                this.location,
                readings.timestamp,
                this.vibrationHistory.getCurrentIntensity()
            )
        );
    }
}

Beyond following the Creator principle, the beehive (UrbanBeeHive) also acts as an Aggregate Root in the context of DDD. This means it is the root of the aggregate and ensures that the consistency of all associated entities and value objects is maintained. In DDD, the aggregate not only encapsulates behavior and data but is also responsible for guaranteeing consistency rules within its context. In our case, UrbanBeeHive creates the alert because it is responsible for analyzing the hive's state, assessing risk, and creating new domain objects.

At first glance, it might seem strange that an entity creates other objects within itself. This happens because, in many traditional architectures or those based on procedural code, object creation is frequently delegated to services, with the flow of control being centralized in external classes. In contrast, in object-oriented design, responsibilities should be assigned to the most natural places: where data and behavior are most intimately related. When we follow this principle, object creation occurs within the entity itself, which already holds the necessary context and data.

In the context of the Creator principle, the responsibility of creation should be assigned to the entity that already has the data and the logic needed to create the object in a cohesive manner. This promotes a more self-contained and cohesive design, where object creation is a natural extension of the entity's behavior, rather than a responsibility that needs to be delegated to external layers. By delegating this creation to the service, we introduce unnecessary coupling and violate the entity's cohesion.

Creating services to manage object creation often adds complexity to the design and results in a separation of responsibilities that is not desirable in object-oriented systems. While services are useful for orchestrating interaction with external systems, delegating object creation to the entity itself allows for a clearer design and avoids the overuse of factories. This is important because factories are often introduced unnecessarily, simply because they are seen as a "best practice," without considering the system's context.

The YAGNI (You Aren't Gonna Need It) principle reminds us not to introduce complexity before it is needed. If creating an object is simple and occurs naturally within the entity's context, as in the case of the swarm risk alert, there is no need to add unnecessary layers with factories or other abstractions. Creating objects in the right place and in a straightforward manner makes the design simpler and more flexible.

Creation Patterns: When and How to Use Them

The Creator principle guides us on who should create objects, but it does not go into details about the how when creation is more complex. This is where creation patterns come into play, helping to organize and structure the object creation process in more complex situations. However, it is important to be careful: do not use creation patterns just because they exist. The pattern should be used when it solves a real creation problem in your design, bringing clarity, organization, and flexibility.

Factory Methods: Expressing Intent in Creation

When creating an object is simple and straightforward, like instantiating a string or a number, a traditional constructor can be perfectly adequate. However, in more complex systems, where object creation involves multiple parameters or additional logic, the use of Factory Methods becomes a more elegant and expressive solution.

Imagine you are building a system to monitor the health of urban beehives. In this system, the beehive not only collects data about temperature and humidity but can also identify risk patterns, such as the possibility of a swarm forming. To notify those responsible, the system needs to generate specific alerts. However, creating these alerts is not a simple task. They can be of the type swarm risk, parasite presence, or temperature anomaly, and each alert type has its own creation rules.

class Alert {
    private constructor(
        public readonly type: AlertType,
        public readonly severity: AlertSeverity,
        public readonly location: RooftopLocation,
        public readonly timestamp: Date,
        public readonly metadata: AlertMetadata
    ) {}

    // Factory methods that express specific use cases
    static swarmRisk(
        location: RooftopLocation,
        timestamp: Date,
        intensity: number
    ): Alert {
        return new Alert(
            AlertType.SWARM_RISK,
            AlertSeverity.CRITICAL,
            location,
            timestamp,
            AlertMetadata.forSwarmRisk(intensity)
        );
    }

    static parasiteDetected(
        location: RooftopLocation,
        timestamp: Date,
        parasiteType: ParasiteType
    ): Alert {
        return new Alert(
            AlertType.PARASITE,
            AlertSeverity.HIGH,
            location,
            timestamp,
            AlertMetadata.forParasite(parasiteType)
        );
    }

    static temperatureAnomaly(
        location: RooftopLocation,
        timestamp: Date,
        reading: Temperature
    ): Alert {
        const severity = reading.isCriticallyLow() || reading.isCriticallyHigh()
            ? AlertSeverity.HIGH
            : AlertSeverity.MEDIUM;

        return new Alert(
            AlertType.TEMPERATURE,
            severity,
            location,
            timestamp,
            AlertMetadata.forTemperature(reading)
        );
    }
}

This is where the power of Factory Methods comes in. Instead of a simple constructor that takes parameters like type, severity, and timestamp, we create named methods specific to each alert type. Now, instead of using new Alert(AlertType.SWARM_RISK, ...), you can call a method like Alert.swarmRisk(location, timestamp, intensity), which already clearly expresses what is happening: a swarm risk is being detected at a given location, with a specific intensity.

The great advantage of using a Factory Method here is that it expresses intent much more clearly than a generic constructor. The method naming already carries the meaning of the object being created. When looking at the code, it is immediately evident that what is happening is not merely the creation of a generic object, but rather a specific event: a swarm risk alert, with all necessary parameters automatically and validly configured.

Furthermore, Factory Methods allow you to encapsulate any logic or validation needed for object creation, keeping the code more organized and easier to maintain. For example, when creating a temperature anomaly alert, we can automatically determine the alert's severity based on the measured temperature. If the temperature is critically low or high, the alert will be severe, while a moderate variation may result in a low severity alert. This type of logic would be difficult to implement cohesively in a simple constructor.

Each Factory Method encapsulates these creation rules, making the code not only more readable but also more robust and flexible. If, in the future, creating a swarm risk alert needs more parameters, such as air humidity for instance, the swarmRisk() method can be easily modified to incorporate this change, without needing to refactor all the code that creates alerts of that type. The flexibility and expressiveness of the method ensure that the system adapts to new requirements without breaking the creation contract that was previously established.

However, the use of Factory Methods goes beyond simple code organization. They represent a design philosophy where each method is an explicit declaration of intent. Instead of treating object creation as a mechanical and decontextualized detail, the Factory Method makes the creation process part of the domain narrative. It expresses, clearly and intentionally, what is being created and why it is being created, aligning the implementation directly with the domain language and the system's needs.

This focus on intent, rather than simply generating data, is a powerful way to communicate the system's behavior and structure. Factory Methods are not merely a technical detail but an extension of the business domain itself, and that is why they become an essential tool when the creation process needs to be more than just instantiating objects.

Builder Pattern: Building Complexity Gradually

The Builder Pattern is especially useful when an object needs to be constructed gradually, with multiple optional parameters or when construction requires multiple steps. Unlike a traditional constructor, which tries to handle all parameters at once, the Builder offers a fluent and controlled API, allowing you to create complex objects in a more readable and flexible manner.

Let us consider the case of a hive assessment report. This report has several components: the hive ID, the assessment date, the health score, a list of alerts, recommendations, sensor data, and even inspection notes. Creating this report with a traditional constructor can result in a confusing and hard-to-understand method signature, especially if the parameters are many or if some of them are optional. This is where the Builder Pattern shines.

Instead of creating a constructor that accepts all these parameters at once, the Builder allows us to add each part of the object incrementally, as information becomes available. In our example, we can build the assessment report fluently, chaining methods that configure the different parts of the object, as shown in the following code:

public class HiveAssessmentReport {

    private final HiveId hiveId;
    private final Date assessmentDate;
    private final HiveHealthScore healthScore;
    private final List<Alert> alerts;
    private final List<Recommendation> recommendations;
    private final SensorDataSnapshot sensorData;
    private final String inspectionNotes;

    // Private constructor
    private HiveAssessmentReport(
            HiveId hiveId,
            Date assessmentDate,
            HiveHealthScore healthScore,
            List<Alert> alerts,
            List<Recommendation> recommendations,
            SensorDataSnapshot sensorData,
            String inspectionNotes
    ) {
        this.hiveId = hiveId;
        this.assessmentDate = assessmentDate;
        this.healthScore = healthScore;
        this.alerts = alerts;
        this.recommendations = recommendations;
        this.sensorData = sensorData;
        this.inspectionNotes = inspectionNotes;
    }

    // Static method to obtain the Builder
    public static HiveAssessmentReportBuilder builder() {
        return new HiveAssessmentReportBuilder();
    }

    // Getters
    public HiveId getHiveId() {
        return hiveId;
    }

    public Date getAssessmentDate() {
        return assessmentDate;
    }

    public HiveHealthScore getHealthScore() {
        return healthScore;
    }

    public List<Alert> getAlerts() {
        return alerts;
    }

    public List<Recommendation> getRecommendations() {
        return recommendations;
    }

    public SensorDataSnapshot getSensorData() {
        return sensorData;
    }

    public String getInspectionNotes() {
        return inspectionNotes;
    }

    // Builder Pattern
    public static class HiveAssessmentReportBuilder {
        private HiveId hiveId;
        private Date assessmentDate;
        private HiveHealthScore healthScore;
        private List<Alert> alerts = new ArrayList<>();
        private List<Recommendation> recommendations = new ArrayList<>();
        private SensorDataSnapshot sensorData;
        private String inspectionNotes;

        public HiveAssessmentReportBuilder forHive(HiveId hiveId) {
            this.hiveId = hiveId;
            return this;
        }

        public HiveAssessmentReportBuilder assessedOn(Date assessmentDate) {
            this.assessmentDate = assessmentDate;
            return this;
        }

        public HiveAssessmentReportBuilder withHealthScore(HiveHealthScore healthScore) {
            this.healthScore = healthScore;
            return this;
        }

        public HiveAssessmentReportBuilder addAlert(Alert alert) {
            this.alerts.add(alert);
            return this;
        }

        public HiveAssessmentReportBuilder addRecommendation(Recommendation recommendation) {
            this.recommendations.add(recommendation);
            return this;
        }

        public HiveAssessmentReportBuilder withSensorData(SensorDataSnapshot sensorData) {
            this.sensorData = sensorData;
            return this;
        }

        public HiveAssessmentReportBuilder addInspectionNotes(String notes) {
            this.inspectionNotes = notes;
            return this;
        }

        public HiveAssessmentReport build() {
            // Required field validation
            if (this.hiveId == null) {
                throw new IllegalArgumentException("hiveId is required");
            }
            if (this.assessmentDate == null) {
                throw new IllegalArgumentException("assessmentDate is required");
            }
            if (this.healthScore == null) {
                throw new IllegalArgumentException("healthScore is required");
            }

            return new HiveAssessmentReport(
                    this.hiveId,
                    this.assessmentDate,
                    this.healthScore,
                    this.alerts,
                    this.recommendations,
                    this.sensorData,
                    this.inspectionNotes
            );
        }
    }
}

// Elegant and readable usage
var report = HiveAssessmentReport.builder()
        .forHive(hiveId)
        .assessedOn(new Date())
        .withHealthScore(healthScore)
        .addAlert(swarmAlert)
        .addRecommendation(immediateInspection)
        .withSensorData(latestSnapshot)
        .addInspectionNotes("Routine inspection")
        .build();

What the Builder essentially offers is the ability to separate the incremental construction of a complex object from its validation and construction logic. In the example above, you can add elements to the report in any order and in a controlled fashion, without the concern of passing a bunch of parameters at once or having to deal with null or undefined values.

Additionally, the Builder allows you to validate parameters efficiently, without overloading the entity's constructor. In the case of the assessment report, if any required field is not filled in, such as the hiveId, the build() method itself throws an error. This ensures that the final object will always be valid, without the need for validations scattered throughout the code.

However, it is important to note that the Builder should be used when the construction complexity is real, and not merely imagined. If the object in question has only a few simple parameters, such as a name and an address, a simple constructor or a factory method is more than sufficient. The Builder comes into play when object construction involves multiple options or when construction needs to be done in stages: that is, when the complexity justifies introducing this structure.

The main advantage of the Builder is clarity. It makes the creation of complex objects more readable and comprehensible, allowing code to be both fluent and safe. Instead of struggling with multiple parameters in a single constructor or creating generic methods to encapsulate different scenarios, the Builder offers an elegant solution for constructing objects with increasing complexity, in a way that the code becomes self-explanatory, well-structured, and easy to maintain.

At its core, the Builder pattern facilitates controlled and flexible object creation, but it is essential not to overuse it. Its true strength lies in the real complexity of the creation process, and its use should be carefully weighed so as not to add unnecessary layers to a system too simple to justify its need.

Avoiding the "Bag of Variables"

A common anti-pattern is the use of a "bag of variables", where data is passed without clear structure or meaning. This type of approach makes the code confusing and hard to understand.

Anti-pattern Example: Bag of Variables

function createAlert(
    type: string,
    severity: number,
    lat: number,
    lon: number,
    building: string,
    rooftop: string,
    timestamp: number,
    data1: number,
    data2: string,
    data3: boolean
): Alert {
    // What on earth are data1, data2, data3?
}

// Incomprehensible call
const alert = createAlert(
    'SWARM',
    3,
    40.7128,
    -74.0060,
    'Building A',
    'North Rooftop',
    Date.now(),
    250,
    'high-intensity',
    true
);

In this code, the parameters have no explicit meaning. The variables data1, data2, data3 are examples of data without context, making it difficult to understand what is being passed and what its role is. Furthermore, the createAlert function has no knowledge of the domain: it does not really know what it is creating, it merely packages meaningless data.

Solution: Using Value Objects and Domain Types

To solve this problem, we can use Value Objects and domain types, which ensure that each variable has a clear meaning and associated behavior. This brings more clarity and structure to the code.

// CORRECT: Types with meaning
interface SwarmAlertData {
    location: RooftopLocation;
    timestamp: Date;
    intensity: VibrationIntensity;
    riskLevel: SwarmRiskLevel;
}

class Alert {
    static swarmRisk(data: SwarmAlertData): Alert {
        return new Alert(
            AlertType.SWARM_RISK,
            AlertSeverity.fromRiskLevel(data.riskLevel),
            data.location,
            data.timestamp,
            AlertMetadata.forSwarmRisk(data.intensity, data.riskLevel)
        );
    }
}

// Call that expresses meaning
const alert = Alert.swarmRisk({
    location: RooftopLocation.fromCoordinates(40.7128, -74.0060, 'Building A', 'North Rooftop'),
    timestamp: new Date(),
    intensity: VibrationIntensity.fromHertz(250),
    riskLevel: SwarmRiskLevel.HIGH
});

In this revised code, we use domain types like VibrationIntensity and SwarmRiskLevel, which are not merely numbers or strings, but value objects with their own meaning and validation. The Alert.swarmRisk() method now accepts an object with clear context (SwarmAlertData), and each value is validated at creation time. This improves code readability and maintainability, making it more expressive and aligned with the domain.

The Creator principle teaches us that the responsibility for creating objects should fall on whoever has the context and the information needed for it. Creation patterns such as Factory Methods and Builder are powerful tools for exercising this responsibility clearly when creation is complex. By adopting these practices, you will notice that the fluency and readability of the code improve significantly, making it more intuitive and easy to understand. The code becomes so clear that even someone with knowledge of the business domain can read and easily understand what is happening.

However, it is important to remember: do not use a pattern just for the sake of using it. Use it when it truly adds clarity and solves a specific creation problem in your design.


Conclusion

By applying the principles of Information Expert and Creator, we can organize our classes and objects so that each one has the correct responsibility, keeping interactions between them minimal and essential, which allows us to create systems that are more flexible, cohesive, and easily understood by both developers and business stakeholders, who can better understand what the code is actually doing.

Now that we have explored how to apply the principles of Information Expert and Creator to distribute responsibilities in software design, a fundamental question arises: how should these responsibilities connect efficiently? How do we ensure that dependencies between classes and components are well managed, without creating an overly interconnected system that is difficult to maintain?

These are challenges that the principles of Low Coupling and High Cohesion help us solve. In the next article, we will discuss how to reduce coupling and increase cohesion to create more organized and flexible systems. We will see that, contrary to what many think, the pursuit of zero coupling is not the ideal solution. What we need is minimal but sufficient connections between components. Moreover, cohesion is not just about what is inside a class, but about how those classes and packages relate within the system.

This software design journey is like the dynamics of a beehive. Bees, although they depend on one another to survive, are not overwhelmed by a web of interdependencies. There is a natural balance of efficient and necessary interactions, allowing the system to function harmoniously. In code, we also seek to achieve this balance, where responsibilities are distributed clearly and efficiently, and dependencies are managed intelligently.

In the next article, we will delve deeper into this balance between coupling and cohesion, exploring how these concepts impact the maintainability and flexibility of complex systems, always with the goal of leveraging business understanding and ensuring that our code is not only technically robust but also fluid to read and aligned with the real needs of the domain.