Introducing Behavior as Data Pattern


When Data Becomes Behavior

In my experience designing complex software systems, I frequently encountered challenges where changing a simple field value affected the overall behavior and logic of an object or module. Initially, these designs were data-driven, meaning those values were just stored as simple fields. Over time, as different behaviors were implemented based on those values, the code became scattered, complex, and difficult to maintain.

In this article, I introduce the Behavior as Data Pattern. This pattern helps us decide when a simple field should be transformed into a behavioral model. I will also discuss key indicators and heuristics that help both developers and product owners recognize this need, accompanied by diverse real-world examples.

This pattern is related to my previews State/Status Segregation(S3) Pattern. You can read it in the link below.

Don’t Confuse State with Status: Lifecycle vs. Context in Domain Modeling

The Moment I Realized This Pattern Was Needed

In one project involving an online ordering system, during a meeting, the product manager said:

The order is still active, but the delivery rider is stuck in traffic, the restaurant hasn’t confirmed it yet, and the user just called to change the delivery address.

A simple sentence but packed with information: the main status of the order, simultaneous temporary conditions, and even changes to order data. Initially, the status field was modeled as an enum with many scattered conditional checks around it.

Initially, we modeled the order’s status simply as an enum:

enum OrderStatus {
Placed,
Preparing,
RiderDelayed,
RestaurantUnconfirmed,
AddressChangeRequested,
Delivered
}

The code was filled with conditionals like:

if (order.Status == OrderStatus.RiderDelayed) {
NotifyUser("Your delivery is delayed due to traffic.");
} else if (order.Status == OrderStatus.RestaurantUnconfirmed) {
NotifyUser("The restaurant has not confirmed your order yet.");
}
// More scattered conditionals handling various statuses...

As requirements grew, these scattered conditionals became unmanageable. Changes in the Status field directly altered how the system behaved across multiple modules, leading to bugs and complexity.

I realized the Status field was no longer just data. It encoded complex behavior and temporary conditions simultaneously. This insight drove us to look for a design that encapsulated these behaviors more cleanly.

By moving from an enum to separate behavioral classes implementing shared interfaces, we achieved more maintainable and extensible code.

We gradually realized this data-centric model was insufficient because changing the status field directly influenced the system’s behavior. We needed a design that cleanly encapsulated these behaviors in a maintainable and extensible way. This realization led me to the Behavior as Data Pattern.

Pattern Definition

The Behavior as Data pattern states:

If the value of a field affects the behavior of an object, then that field is not just data, it should be transformed into a behavioral model.

In other words, when fields such as status, type, or any other value directly determine the system’s logic and operations, keeping them as primitive fields leads to complexity and maintainability problems. The solution is to introduce separate classes or interfaces modeling these distinct behaviors.

In the next article I’ll talk more about refactoring techniques and different ways to implement Behavior as Data Pattern.

When to Transform a Field into Behavior: Clues and Heuristics

Determining whether a field should remain simple data or become behavior is not always obvious. Here are key heuristics to guide developers and product owners:

  • Does changing the value of this field alter the system’s logic or behavior? If yes, consider behavioral modeling.
  • Are there numerous scattered if/else or switch statements based on this field? A sign that behavior is entangled and needs refactoring.
  • Is the set of possible field values likely to grow over time? Growing sets of values usually cause condition explosion.
  • Is testing behavior complicated and tightly coupled to the field’s value? If tests require complex setup or many cases because of field values, consider behavioral modeling.
  • Does this field represent an important domain concept that triggers specific behaviors? If it triggers domain-specific processes, notifications, or rules, model it as behavior.

Examples

1. Bank Account Currency

Before (Data-driven):

class BankAccount {
String currency; // "USD", "EUR" // even if its type was an enum

Money convertTo(String targetCurrency) {
if (currency.equals("USD") && targetCurrency.equals("EUR")) { ... }
else if (...) { ... }
}
}

Problem: Currency conversion logic scattered and grows as currencies increase.

After (Behavior-driven):

interface Currency {
  Money convertTo(Money amount, Currency target);
}
class USD implements Currency { ... }
class EUR implements Currency { ... }
class BankAccount {
  Currency currency;
  Money convertTo(Currency target) {
    return currency.convertTo(this.balance, target);
  }
}

Now, currency is not just data but a behavioral model.

2. Tax Calculator

Before:

class Invoice {
String country;
Decimal calculateTax() {
if (country.equals("DE")) return amount * 0.19;
if (country.equals("UK")) return amount * 0.2;
...
}
}

After:

interface TaxPolicy {
BigDecimal calculateTax(Decimal amount);
}

class GermanyTaxPolicy : TaxPolicy { ... }
class Invoice {
TaxPolicy taxPolicy;
Decimal calculateTax() {
return taxPolicy.calculateTax(this.amount);
}
}

Here, country becomes a behavioral model representing tax policy.

3. UI Button Style

Before:

<button class={isPrimary ? "primary" : "secondary"}>

After:

class Button {
ButtonStyle style;

render() {
return `<button class="${style.cssClass()}">`
}
}

When a field influences rendering and behavior, it deserves behavioral modeling.

4. Airline Ticket Payment Methods

class PaymentMethod {
String method; // "wallet", "bank", "installment", "combined"
}

Simple field is fine only if logic is trivial. When complex payment logic exists for each method:

interface PaymentStrategy {
void pay(Order order);
}

class WalletPayment implements PaymentStrategy { ... }
class BankPayment implements PaymentStrategy { ... }
class InstallmentPayment implements PaymentStrategy { ... }
class CombinedPayment implements PaymentStrategy { ... }

class Order {
PaymentStrategy paymentMethod;
void pay() {
paymentMethod.pay(this);
}
}

Relation to State/Status Segregation Pattern (S3)

This pattern, often discussed in complex systems and featured in my upcoming book Language-Driven Design, advocates separating status and behaviors tied to it. It complements Behavior as Data by providing a concrete approach to managing states and their associated behaviors, reducing conditional complexity.

Practical Advice for Teams

  • Look for scattered conditionals during code reviews.
  • Host modeling sessions with developers and product owners to clarify domain concepts.
  • Document domain concepts explicitly to reflect their behavioral aspects.
  • Use static analysis or complexity metrics to detect conditional explosion.

The Last Words!

Recognizing when a field is not mere data but a behavior trigger is crucial for designing flexible, testable, and maintainable software. The Behavior as Data pattern teaches us to separate behavior from passive data and transform fields that drive logic into distinct behavioral models. This reduces complexity, improves clarity, and prepares the system for future changes.

This pattern is a key part of the design philosophy explored in my upcoming book Language-Driven Design.

Leave a Reply

Your email address will not be published. Required fields are marked *