Model Tension
Heuristics

Preventing Accidental Design Debt

@julientopcu.com

‪@josianchevalier.bsky.social‬

CHAPTER I

When model exploration
   Whirlpool 
      turns into
 model sinking

TODo

Current Quest

Done

Overlapping ships can be selected

Bug

[Paid Option] Wireless pidgin connexion

Feature

JIRA

Ships departing in the past appear in Search

Bug

Add cancellation insurance

Feature

TODo

Current Quest

Done

Overlapping ships can be selected

Bug

[Paid Option] Wireless pidgin connexion

Feature

JIRA

Ships departing in the past appear in Search

Bug

Add cancellation insurance

Feature

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "AISLE", "WINDOW", "PANORAMIC_VIEW", "LOUNGE" ],
            "deckOptions": [ "ANY", "UPPER_DECK", "MIDDLE_DECK", "LOWER_DECK" ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true
          },
          {
            "comfortClass" : "SECOND",
            "price": "50€",
            "seatOptions": [ "ANY", "AISLE", "WINDOW" ],
            "deckOptions": [ "ANY", "MIDDLE_DECK", "LOWER_DECK" ],
            "selected": false,
            "booked" : false
          }
        ]
      },
      {
        "number": "B",
        "bound": "outbound",
        "departureDate": "2025-10-06T15:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "80€",
            "selected": false,
            "booked" : false
          }
        ]
      },
      {
        "number": "C",
        "bound": "inbound",
        "departureDate": "2025-10-16T12:00",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "90€",
            "seatOptions": [ "ANY", "AISLE", "WINDOW", "PANORAMIC_VIEW" ],
            "deckOptions": [ "ANY", "UPPER_DECK", "LOWER_DECK" ],
            "preferences": {
              "seat": "PANORAMIC_VIEW",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked": true
          }
        ]
      },
      {
        "number": "D",
        "bound": "inbound",
        "departureDate": "2025-10-16T14:00",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "AISLE", "WINDOW", "PANORAMIC_VIEW" ],
            "deckOptions": [ "ANY", "UPPER_DECK", "LOWER_DECK" ],
            "selected": false,
            "booked" : false
          },
          {
            "comfortClass" : "SECOND",
            "price": "50€",
            "seatOptions": [ "ANY", "AISLE", "WINDOW" ],
            "deckOptions": [ "ANY", "LOWER_DECK" ],
            "selected": false,
            "booked" : false

          }
        ]
      }
    ]
  }
}

drives

Exploration

&

Experimentation

Business Need

shape

Model

Searching

drives

Exploration

&

Experimentation

Business Need

shape

Model

needs to account for new

Selecting

drives

Exploration

&

Experimentation

Business Need

shape

Model

needs to account for new

Selecting

drives

Exploration

&

Experimentation

Business Need

shape

Model

needs to account for new

Selecting

drives

Exploration

&

Experimentation

Business Need

shape

Model

needs to account for new

Booking

drives

Exploration

&

Experimentation

Business Need

shape

Model

Model Exploration
Whirlpool

needs to account for new

drives

Exploration

&

Experimentation

Business Need

expand

Model

needs to account for new

The Mighty

Big Ball Of Mud

drives

Exploration

&

Experimentation

Business Need

Model

needs to account for new

expand

Model Exploration
Whirlpool

drives

Exploration

&

Experimentation

Business Need

Model

BBOM

needs to account for new

collapses into

expand

Model
Sinking

Accidental Design Debt:
the invisible engine of BBOMs

public record Search(UUID id, Criteria criteria, SearchType searchType, List<Ship> ships) {

    public Search {
+ 		if(!hasBookedShips()){
          assert ships.stream()
          			.noneMatch(ship -> ship.departureDate().isBefore(now()))
                : "Some ships are departing in the past";
+ 		}
    }
}

Fix Booking history regression #2097

Josian merged 10 commits into

cruising:main

+ 532  -385

public record Search(UUID id, Criteria criteria, SearchType searchType, List<Ship> ships) {

    public Search {
-		if(!hasBookedShips()){
-         assert ships.stream()
-         			.noneMatch(ship -> ship.departureDate().isBefore(now()))
-               : "Some ships are departing in the past";
 		}
    }
}

Fix "Previous Days Ships" feature #2099

Josian merged 57 commits into

cruising:main

+ 1278  -985

drives

Exploration

&

Experimentation

Business Need

BBOM

Solution-Push

focuses on

Obsolete

Model

needs to account for new

collapses into

expand

drives

Exploration

&

Experimentation

Business Need

shape

Model

$$$

needs to account for new

No model is Inherently extensible !

Exploration

&

Experimentation

Business Need

Model

needs to account for new

Business Need

Model

Model Exploration
Whirlpool

challenged by new

Not a confirmation bias

Exploration

&

Experimentation

needs to account for new

Business Need

by shaping

Model

New Model

may

Depreciate

Exploration

&

Experimentation

challenged by new

One does not simply

challenge the model

Accidental
Design Debt

The Ruthless

Accidental Design Debt

Model

Accidental
Design Debt

generates through


misguided decisions

manifests

as complexity in

lower their cohesion and flexibility,

increase their complexity,

and erode their correctness.

introducing coupling in models,

from Decisions that inadvertently

Features taking longer to develop?

Model

Accidental
Design Debt

generates through


misguided decisions

manifests

as complexity in

Features taking longer to develop?

Simple change causing widespread model updates?

Model

Accidental
Design Debt

generates through


misguided decisions

manifests

as complexity in

Features taking longer to develop?

More bugs in unrelated parts of the model?

Simple change causing widespread model updates?

Model

Accidental
Design Debt

generates through


misguided decisions

manifests

as complexity in

Model

Accidental
Design Debt

Model
Entropy

produces through

accumulation

Features taking longer to develop?

More bugs in unrelated parts of the model?

Simple change causing widespread model updates?

generates through


misguided decisions

manifests

as complexity in

The Unyielding

Model entropy

Features taking longer to develop?

More bugs in unrelated parts of the model?

Simple change causing widespread model updates?

Model entropy

High Entropy

Design effort is dissipated on navigating accidental complexity

Degree of design disorder

low coherency, consistency, cohesiveness

unclear outcomes

unexpected regressions

dissipates the structure
and clarity of

Model
Entropy

produces through accumulation

Model

Accidental
Design Debt

generates through


misguided decisions

dissolves the model into a

BBOM

Model
Entropy

produces through accumulation

dissipates the structure
and clarity of

Model
Sclerosis

hardens into

locking the model into

Model

Accidental
Design Debt

generates through


misguided decisions

Spork Effect:
unveiling the roots of model fragility and rigidity

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Booking

Search

Accidental
Design Debt

Responsibilities
Coupling

may induce

strong

manifests as

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Intrinsic
Coupling

Accidental
Design Debt

Responsibilities
Coupling

may induce

Booking

Search

manifests as
strong

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Intrinsic
Coupling

Accidental
Design Debt

Responsibilities
Coupling

may induce

Search

Booking

manifests as
strong

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Booking

Search

Intrinsic
Coupling

Accidental
Design Debt

Responsibilities
Coupling

may induce

manifests as
strong

Spork  effect

Intrinsic
Coupling

manifests as
strong

Accidental
Design Debt

Responsibilities
Coupling

may induce

"Spork Effect"

"Spork Effect"

causes

Model
Fragility

Intrinsic
Coupling

manifests as
strong

Accidental
Design Debt

Responsibilities
Coupling

may induce

Model
Rigidity

leads to

Model
Entropy

increases

produces
through accumulation

{}

Booking

Search

Intrinsic
Coupling

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Booking

Search

Booking

Search

Booking

Distinct Models!

Search

Booking

Booking

Search

Booking

Distinct Models!

Search

Booking

Booking

"Spork Effect"

Intrinsic
Coupling

Refactoring
Habits

fosters

KISS
DRY
YAGNI

{}

Sunk Cost
Fallacies

fosters

Deadlines
Pressure

"Spork Effect"

Intrinsic
Coupling

{}

Refactoring
Habits

fosters

Sunk Cost
Fallacies

fosters

Deadlines
Pressure

KISS
DRY
YAGNI

is the path to

BBOM

"Spork Effect"

Intrinsic
Coupling

{}

BBOM

leads to

Model
Entropy

dissolves the model into a

"Spork Effect"

Intrinsic
Coupling

{}

BBOM

Model
Entropy

dissolves the model into a

leads to

locking the
 

model into

hardens into

Model
Sclerosis

Suffering...

Suffering...

Suffering...

Model tension as signals of accidental design debt

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

ModelTension

The Fearsome

ModelTension

is a structural stress within a model that signals it is being stretched beyond its conceptual integrity.

conceptual integrity

Model
Tension

is the result

of

Accidental
Design Debt

Intrinsic
Coupling

may
induce

Conceptual Integrity

 is the degree to which a model maintains clarity, coherence, cohesion                            

and its purpose.

is the result

of

Accidental
Design Debt

Model
Tension

Conceptual
Integrity

erodes

prevents

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Model
Tension

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true
{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Model
Tension

Booking

Search

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "travelInsurance" : "40€",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "seatOptions": [ "ANY", "..." ],
            "deckOptions": [ "ANY", "..." ],
            "preferences": {
              "seat": "LOUNGE",
              "deck" : "UPPER_DECK"
            },
            "selected": true,
            "booked" : true

Booking

Search

Booking

Selection

Selection

Model
Tension

Crystallized
ModelTension

The Unstoppable

compromises

"fibrosing"

left unresolved

leads to

Model
Tension

Conceptual
Integrity

erodes

Crystallized
Model Tension

Model
Entropy

raises

Model
Sclerosis

hardens into

shapes

left unresolved

leads to

Model
Tension

Crystallized
Model Tension

Model
Obsolescence

signals

ModelTension

as signal of accidental design debt

The Fearsome

Entropy

Tensions
Resolution

Accidental
Design
Debt

Model Rigidity

Entropy

Accidental
Design
Debt

Exploring (intrinsic)
model tensions heuristics

visual elements © Alberto Brandolini

Search
Bound

Select
a Fare on a Ship

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Check Selected Fares
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Ship + Fare Selected On Bound

visual elements © Alberto Brandolini

Ship
& Fare
Selected On Bound

Select
a Fare on a Ship

Selected Fares

Search
Bound

Select
a Fare on a Ship

Ship + Fare Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Check Selected Fares
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

visual elements © Alberto Brandolini

Search
Bound

Select
a Fare on a Ship

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Check Selected Fares
Completion

Linguistic Tension

Salience Bias

Salience Bias

Linguistic Tension

A cognitive bias that predisposes people to focus on  information that are more prominent or visible

visual elements © Alberto Brandolini

Search
Bound

Select
a Fare on a Ship

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Check Selected Fares
Completion

Search
Bound

Select
a Fare on a Ship

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Ancillaries

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Selected Fares

Ancillaries

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Price
Selection

Total Price Computation

Selection
Priced

Check
Completion

Fares Selection
COmpleted

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Selection

Priced
Selection

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Check
Completion

Fares Selection
COmpleted

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Price
Selection

Total Price Computation

Selection
Priced

Priced
Selection

Selection

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Check
Selection
Completion

Check
Selection
Completion

Selection
COmpleted

Selected Fares

Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Incomplete
Selection

complete
Selection

Price
Selection

Total Price Computation

Selection
Priced

Priced
Selection

Selection

(Selection)

visual elements © Alberto Brandolini

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Check
Selection
Completion

Check
Selection
Completion

Selection
COmpleted

Selected Fares

Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Incomplete
Selection

complete
Selection

Price
Selection

Total Price Computation

Selection
Priced

Priced
Selection

Selection

(Selection)

visual elements © Alberto Brandolini

Search
Bound

Select
a Fare on a Ship

Ship + Fare
Selected On Bound

Compute Total Price

Total Price Computation

Total Price Computed

Check
Completion

Fares Selection
COmpleted

Selected Fares

Selected Fares

Fares Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Incomplete
Selection

complete
Selection

Priced
Selection

visual elements © Alberto Brandolini

Check Selected Fares
Completion

Life cycle Tension

Implicit Life cycle

Ancillary
Ship

Select
Ancillary

Ancillary
Selected

Search
Bound

Ship + Fare
Selected On Bound

Check
Selection
Completion

Check
Selection
Completion

Selection
COmpleted

Selected Fares

Selection Found Incomplete

Ancillaries

Retrieval

List
Ancillaries

Ship

Fare

Ancillaries
Listed

Ancillaries

Select
a Fare on a Ship

Incomplete
Selection

complete
Selection

Price
Selection

Total Price Computation

Selection
Priced

Priced
Selection

Selection

(Selection)

visual elements © Alberto Brandolini

Select
Ancillary

Check
Selection
Completion

Selection
COmpleted

Selection Found Incomplete

Price
Selection

Selection
Priced

Select
a Fare on a Ship

Ancillary
Selected

Ship + Fare
Selected On Bound

SELECTION

visual elements © Alberto Brandolini

{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "selected": true,
            "booked" : true
{
  "search": {
    "id": "XXX",
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "fares": [
          {
            "comfortClass": "FIRST",
            "price": "100€"
          },
          {
            "comfortClass": "SECOND",
            "price": "50€"
          }
        ]
      },
      {
        "number": "B",
        "bound": "outbound",
        "fares": [
          {
            "comfortClass": "FIRST",
            "price": "80€"
          }
        ]
      },
      {
        "number": "C",
        "bound": "inbound",
        "fares": [
          {
            "comfortClass": "FIRST",
            "price": "90€"
          }
        ]
      },
      {
        "number": "D",
        "bound": "inbound",
        "fares": [
          {
            "comfortClass": "FIRST",
            "price": "100€"
          },
          {
            "comfortClass": "SECOND",
            "price": "50€"
          }
        ]
      }
    ]
  }
}
{
  "selection": {
    "id": "YYYY",
    "isComplete": true,
    "totalPrice": "190€",
    "outbound": {
      "number": "A",
      "fare": {
        "comfortClass": "FIRST",
        "price": "100€"
      }
    },
    "inbound": {
      "number": "C",
      "fare": {
        "comfortClass": "FIRST",
        "price": "90€"
      }
    }
  }
}
class Selection {
  TripType tripType;
  Map<Bound, Fare> selectedFares = new HashMap();

  void selectedFare(Bound bound, Fare selectedFare) {
    selectedFares.put(bound, selectedFare);
  }

  Price totalPrice() {
    return selectedFares.values().stream()
            .reduce(Fare::add)
            .get();
  }

  boolean isSelectionComplete() {
    if (tripType == ONEWAY){
        return selectedFares.size() == 1;
    } else {
        return selectedFares.size() == 2;
    }
  }
}
public record Search(UUID id, Criteria criteria, SearchType searchType, List<Ship> ships, PaymentMean paymentMean) {

    public Search {
            assert ships.stream().noneMatch(ship -> ship.departureDate().isBefore(now())) 
				: "Some ships are departing in the past";
    }
    /*...*/
}
public record Search(UUID id, Criteria criteria, SearchType searchType, List<Ship> ships, PaymentMean paymentMean) {

    public Search {
        if (!hasBookedShips()) {
            assert ships.stream().noneMatch(ship -> ship.departureDate().isBefore(now())) 
				: "Some ships are departing in the past";
        }
    }
}

public record Search(UUID id, Criteria criteria, SearchType searchType, List<Ship> ships, PaymentMean paymentMean) {

    public Search {
        if (!hasBookedShips()) {
            assert ships.stream().noneMatch(ship -> ship.departureDate().isBefore(now())) 
				: "Some ships are departing in the past";
        }
    }

    public boolean hasBookedShips() {
        Map<Bound, Long> bookedShipsPerBound =
                ships.stream()
                        .flatMap(ship -> ship.fares().stream().filter(Fare::booked).map(_ -> ship))
                        .collect(groupingBy(Ship::bound, counting()));

        if (searchType == SearchType.ONEWAY) {
            return bookedShipsPerBound.get(OUTBOUND) == 1;
        } else { //RoundTrip
            return bookedShipsPerBound.get(OUTBOUND) == 1 && bookedShipsPerBound.get(INBOUND) == 1;
        }
    }

    public List<Ship> getBookedShips() {
        var bookedShips = ships.stream().filter(ship -> ship.fares().stream().anyMatch(Fare::booked)).toList();

        if (bookedShips.isEmpty()) {
            return Collections.emptyList();
        }

        List<Ship> cleanedShips = new ArrayList<>();
        for (var bookedShip : bookedShips) {
            Fare bookedFare = getBookedFare(bookedShip);
            Ship cleanedShip = new Ship(bookedShip.number(), bookedShip.bound(), bookedShip.departureDate(), asList(bookedFare));
            cleanedShips.add(cleanedShip);
        }
        return cleanedShips;
    }

    private Fare getBookedFare(Ship bookedShip) {
        Fare bookedFare = null;
        int i = 0;
        while(i < bookedShip.fares().size()) {
            Fare fare = bookedShip.fares().get(i);
            if(fare.booked()){
                bookedFare = fare;
            }
            i++;
        }
        return bookedFare;
    }

    public void book() {
        if(paymentMean == null) {
            throw new IllegalStateException("Payment mean is missing");
        }

        if (hasBookedShips()) {
            throw new IllegalStateException("Ships are already booked");
        }
        var selectedFaresPerBound = new ArrayList<Entry<Bound, Fare>>();
        for (Ship ship : ships) {
            for (Fare fare : ship.fares()) {
                if (fare.selected()) {
                    selectedFaresPerBound.add(entry(ship.bound(), fare));
                }
            }
        }

        if (selectedFaresPerBound.isEmpty()) {
            throw new IllegalStateException("Ships must be selected before booking");
        }

        var numberOfSelectedFarePerBound = selectedFaresPerBound.stream().collect(groupingBy(Entry::getKey, counting()));
        if (searchType == SearchType.ONEWAY) {
            assert numberOfSelectedFarePerBound.get(OUTBOUND) == 1 && numberOfSelectedFarePerBound.get(INBOUND) == 0 : "Inconsistent number of selected fares";
        } else { //RoundTrip
            assert numberOfSelectedFarePerBound.get(OUTBOUND) == 1 && numberOfSelectedFarePerBound.get(INBOUND) == 1 : "Inconsistent number of selected fares";
        }

        for (Entry<Bound, Fare> selectedFare : selectedFaresPerBound) {
            selectedFare.getValue().book();
        }
    }

    public boolean isSelectionComplete() {
        Map<Bound, Long> selectedFaresPerBound = ships.stream().flatMap(ship -> ship.fares().stream().filter(Fare::selected).map(_ -> ship)).collect(groupingBy(Ship::bound, counting()));

        var selectedOutBounds = selectedFaresPerBound.get(OUTBOUND);
        var selectedInBounds = selectedFaresPerBound.get(INBOUND);

        if (searchType == SearchType.ONEWAY) {
            //This should never happen 👇
            assert selectedOutBounds <= 1 && selectedInBounds <= 0 : "Too many fares are selected";
            return selectedOutBounds == 1;
        } else { //RoundTrip

            //This should never happen 👇
            assert selectedOutBounds <= 1 && selectedInBounds <= 1 : "Too many fares are selected";
            return selectedOutBounds == 1 && selectedInBounds == 1;
        }
    }

    public Price getTotalPrice() {
        return ships.stream().flatMap(ship -> ship.fares().stream().filter(Fare::selected).map(Fare::price)).reduce(Price::add).orElse(Price.ZERO);
    }

    public void selectFareOfShip(Bound bound, String shipNumber, ComfortClass comfortClass) {
        var fareToSelect = ships.stream().filter(ship -> ship.number().equals(shipNumber) && ship.bound() == bound).flatMap(ship -> ship.fares().stream().filter(fare -> fare.comfortClass() == comfortClass)).findFirst().orElseThrow();

        var previousSelectionOnBound = ships.stream().filter(ship -> ship.bound() == bound).flatMap(ship -> ship.fares().stream()).filter(Fare::selected).findFirst();

        previousSelectionOnBound.ifPresent(fare -> fare.setSelected(false));
        fareToSelect.setSelected(true);
    }
}
{
  "search": {
    "id": "XXXX",
    "criteria": {
      "outBoundDate": "2025-10-06T12:00",
      "inBoundDate": "2025-10-16T11:00"
    },
    "selectionComplete" : true,
    "totalPrice" : "230€",
    "paymentMean": {
      "type": "CREDIT_CARD",
      "number": "***********"
    },
    "ships": [
      {
        "number": "A",
        "bound": "outbound",
        "departureDate": "2025-10-06T13:30",
        "fares": [
          {
            "comfortClass" : "FIRST",
            "price": "100€",
            "selected": true,
            "booked" : true
public record Search(UUID id, Criteria criteria, SearchType searchType, List<Ship> ships) {

    public Search {
		assert ships.stream().noneMatch(ship -> ship.departureDate().isBefore(now())) 
			: "Some ships are departing in the past";
    }

   /*...*/
}
public record Booking(UUID id, List<Ship> ships, PaymentMean paymentMean, Boolean finalized) {

	public Booking {
    	if(!finalized){
        	assert ships.stream().noneMatch(ship -> ship.departureDate().isBefore(now())) 
				: "Some ships are departing in the past";
        }
    }

   boolean isFinalized(){/**/}
}

CHAPTER 2

When model sclerosis  
      turns into model

Fragmentation

CRUISING

POST-BOOKING

Text

works around

Sclerosed
Model

Outgrowth
Model

POST-BOOKING

CRUISING

Search

Exchange

Cancellation

Search

Fare

Booking

Selection

Selection

Booking

POST-BOOKING

CRUISING

Search

Exchange

Cancellation

Selection

Booking

POST-BOOKING

CRUISING

Search

Exchange

Cancellation

Search

Booking

Selection

Selection

Booking

Search

Exchange

Cancellation

noDepartureInThePast()

noDepartureInThePast()

POST-BOOKING

CRUISING

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

POST-BOOKING

CRUISING

Selection

Booking

Search

Exchange

Cancellation

POST-BOOKING

CRUISING

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Booking

Exchange

Selection

Booking

Search

Exchange

Cancellation

Cancellation

Booking

Exchanged
Booking

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Cancelled
Booking

Selection

Booking

Search

Exchange

Cancellation

POST-BOOKING

CRUISING

Booking

Exchanged
Booking

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Cancelled
Booking

Completed
Booking

Selection

Booking

Search

Exchange

Cancellation

POST-BOOKING

CRUISING

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Created

Completed

Exchanged

Cancelled

Selection

Booking

Search

Exchange

Cancellation

POST-BOOKING

CRUISING

Booking Lifecycle

Created

Completed

Exchanged

Cancelled

POST-BOOKING

CRUISING

Booking

Exchange

POST-BOOKING

CRUISING

Seat Preferences

Seat Preferences

Cancellation

Selection

Booking

Search

Exchange

Cancellation

POST-BOOKING

CRUISING

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Booking

Exchange

Cancellation

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nI1WwXLbNlx1MDAxML37KzTqNUWw2Fx1MDAwNbDIrU56yKXTiafTznRyoCVaYkyTXHUwMDFhkorjZvzvfaRcdTAwMTRRVO1hdbAlLPbh7e7bXHUwMDA1vl8tXHUwMDE2y+5ply/fLZb5t1VWXHUwMDE26yZ7XFy+6de/5k1b1Fx1MDAxNUxu+N3W+2Y17Nx23a599/bt6GFW9cPBKy/zh7zqWuz7XHUwMDFiv1x1MDAxN4vvw19YinXv++U6v/3rhp++fGhcdTAwMWWLrdebT9lcdTAwMWafXHUwMDA212HTXHUwMDBmMmVR5ePqNyxcdKlcdTAwMDajkb2N1obAJ+tcdTAwMTOs5JWcIbXsOJKLdLI+XHUwMDE2627b74CT8YnJJ1x1MDAxN9W7OFx1MDAwMmzzYrPtsIdZjWhyNqlcdTAwMGJqddySVZuyJ2ZPK23X1Pf5+7qsm57wT7llYlx1MDAxYTnfZqv7TVPvq/Xre+6KsrzpnspDVrPVdt+cRX044c8jf3exfvJra9Rg9MKRm22Vt+2Ebb3LVkX3dMjDabXnt/u4XHUwMDFlivV5ZNVkXHUwMDBm+ce+WtW+LE/LRbXO+0Isb5t/JsdV6+NxP0o91tFcdTAwMWRXnkfyed4jqziHMvrT+qg2ZuHL5d/qapBcdTAwMWVFXHUwMDE3nNiY4kir/Vx1MDAwMM11XHUwMDAz6l1WtvlYgZ7ar2d6XHUwMDFjQ9zv1tnBhaKoaFRmXHUwMDEyPdkhv/vL+Mt6df/CKbu6OJd7/1x1MDAxOb8txlx1MDAxMlxmP07fP795cTcn41x1MDAxMrlAwslcdTAwMDeb6Nz751x1MDAxNExcdTAwMDBdXHUwMDBiXHUwMDBiTEFkXHUwMDBlTtSQ2OCdXHUwMDE3QKaY/Fx1MDAwNI9cIlx1MDAxOU1cdTAwMTLYojc0OuU5QLJkXHUwMDFjh5RcdTAwMTKpXHUwMDBmnMZKXHUwMDFkXHUwMDAwVYxccjjNxYhcbkeaw3PEhskpXHUwMDFhLiiopilcXFCTlNE0SaOQ1TibP8smhKDOXCJPkdVdXHUwMDA0TN5cYsRcdTAwMTMoWsgopFk8TSaJsorlmJyfhitkOFx1MDAwNMJHJUbHs+FKSIbYXHUwMDA2TFx1MDAxNlEl0JjiReNcdTAwMTGu9ZpQsjRcdTAwMWKtXHUwMDE3b5R9XHUwMDEy21x1MDAwZj6+UEtU45BSdk6QW/WzxVxyYqGvXHUwMDFlXHUwMDA3XHRKYDHNnfeGbITNOzRcdTAwMGJYzuEh0YaVI/6z+kiXWkHpUVxuXHUwMDFjhFlA3oY5PCRcdTAwMDXzXHUwMDFmiUNcdTAwMDKtI1xyUzwnJoq3ySNcdTAwMWKqmPPzYn7tRjhcdTAwMTI0PnhcdTAwMTboXFyciEuzgFx0/YRcdTAwMWVTSdhcdTAwMWODm+DBhk5cdTAwMWKaQygw6SxcdTAwMWF6jZxVSlx1MDAxMTknL+do5PpqeGLvNNggaVx1MDAxNi7GXGKPhEiQoVx1MDAwNDVP4LxcdTAwMThx6Jxoe7WTm9WKS9AqWSRcXFx1MDAxMqWpkMHaQFwiXlLCuKZ55Xmrhq2mZK2ilZgncFxi1VqBjokkoG/jLFx1MDAxZatFYYODUFx1MDAwM0fnJ0pxUCVcbqtOUt+EXHUwMDFh/8dUXHSYQ76XXHUwMDAxcbQ8gcNRuNQtQSr90JFZco7xUFx0jOyhXHUwMDBlooh9mjs1XHUwMDAxQmFFNTxcdTAwMDbz7FBBq2JcbkSrIIdcdTAwMTGP6+NcdTAwMWNcdTAwMGZ3XHUwMDA2Olxml6Z6j7mi811BmFx1MDAwMpg+LkFZYFx1MDAxMya16EfEyI7mb4zX7r+rXHUwMDBir2WZtd37+uGh6HAx/95fqpdcdTAwMTdw22VNd41nSFFtLm15tX7FMnj90jT14zbP/vOogd+rtl1dPm2GJ0nX7PNh9fnqSHmZ7XY3XHUwMDFk3lx1MDAxMKdnz/JrkT9ev/Dou1x1MDAxYj7Lq+MrqH/05cNr6fnq+V/8q0SQIn0=

Extrinsic Coupling

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nI1WTW/bRlx1MDAxML37V1xi6jXd7M7sx0xuddKiuVx1MDAwNC2SXCKHXCJcdTAwMDdGoiXCNCmQVGwh8H/vI62Iolx1MDAxY4PlQVwid2bfzs68+fh+tVgsu8MuX75ZLPOHVVZcdTAwMTbrJrtfvurXv+VNW9RcdTAwMTVENHy39b5ZXHKa267btW9ev1x1MDAxZXeYVX33tCsv87u86lro/YvvxeL78Fx1MDAwYkmx7veG61x1MDAwZvff2j/efb752316+Och/HnY2GHroPTDmLKo8nH1XHUwMDAxS55scMY6XCJOITlcdTAwMTfDSXyA2EUvyTgrXHUwMDFhyHtcdEwn8X2x7ra9irXRXHUwMDA0ZVx1MDAxN5SSXHUwMDA0SnxS2ebFZttBh1mMXHUwMDE3JatCUayMKlm1KXvT7Gml7Zr6Nn9bl3XTm/zLzc1aY1x1MDAxY63+mq1uN029r9Ynna7JqnaXNfDQqHdTlOXH7lA++TZbbffN2d2fTvl8vIO/WD/ta2tEYtyFYzfbKm/bicX1LltcdTAwMTXd4clcdTAwMTen1d7G3fv1XHUwMDEwsi+jVU12l7/vY1bty/K0XFxU67xcdTAwMGbH8mtrJ8dV6+NxP1x1MDAwMj5Gk44rj6Pxed4jO++sXHUwMDBilml088g65vBs+UNdXHJcdTAwMTQki3h7XHUwMDA0UUfD2nfgXjfg3mRlm49x6I37/YyX4yX3u3X2tMUlL15EQVx1MDAwMDcyXHUwMDBiNLy99EBZr25/csquLs5p3z/j22JcZsLwcXr/8uqn2qyG1FF0njVEq+58969cdTAwMWFNhLlcdTAwMTZcdTAwMTKI4Ig5OC/GeVx1MDAxYlx1MDAwM1x1MDAwNVx1MDAwZkhNXHUwMDFhJnguOSPqI1tkiCRcdTAwMTKeXHUwMDAzRNhcZnFUVSchsnqeXHUwMDAyijc24jRKSfDj5vDIsWFHgrSLXHUwMDAyU3Vcblx1MDAxN8WosGWnkkBcdTAwMTlJs/6zbGKMQlx1MDAxNn5KLHRxYVx1MDAxN4xPmqJLluBcdTAwMTidxVx1MDAxMzXqhcVbTkphel3vXGbH6PCIT4l49ro+qnFsI+pcdTAwMGI452DGXHUwMDE0L5mA69rQXHUwMDE3s6Czt1xyPlx1MDAxOOGg3jqxzFx1MDAxN2xJYlxiLmVcIlx1MDAwZt+iMs7BRW/Br1x1MDAxZVx1MDAwN1x1MDAwZVJYMfVdXGIos1x0skDMqCNhXHUwMDBlXHUwMDBmjjYsnPDPgtJ9yVx1MDAxNYRcdTAwMWWhwEGeXGLVIM7hwSlcdTAwMDY8XHUwMDEwgVx1MDAwMy05iVM88ib5YDXAXHUwMDFiXCKo9vNkfqkvXHUwMDFjXHI0IVx1MDAwNvbguUdrIZ1cdTAwMDVU5Fx1MDAxM3JMvEI5RZrgQYZMXHUwMDFiksO7yE5m0ZBrjqw4TfC5XHUwMDBi/lx1MDAxY81RXHUwMDFmjeA4kERcdTAwMWK9zsKl1LdJxU3gIVx1MDAwNZsncMFcdTAwMWJPyJxke7Y7muVcbim46ixcdTAwMWPu1emUyLDagFwiwatSRGhnmVx1MDAxN6xcdTAwMThGXHUwMDBiV2tcdTAwMDWpxDyBw1Wt9eCxcz5cIm/TLFx1MDAxZYtFYCNGXHUwMDA2jpwoTJhCYCVcdTAwMDIr5LVPQkn/o6pE1KHQ08BxsjyBw1GMnMZcXMJ90fGzxlx1MDAxMZNcdTAwMDFcdTAwMDHgPcRcdTAwMDFcdTAwMTOLlanvxERcdTAwMTCFXHUwMDA10VxiKMyzRVx1MDAwNamKKpCswDiUeLSPczz0XGZkWCRcdTAwMWNcdTAwMTRQV2Q+K1x1MDAxY6pcdTAwMDCqXHUwMDBmKZhcdTAwMDVr4iRcdTAwMTZ9iVx1MDAxOK1z81x1MDAxZOOl/nd1sWtZZm33tr67Kzo05r/6pnrZgNsua7prXGZcIkW1uZTl1fpcdTAwMDXJsOu3pqnvt3n2bKzBvlx1MDAxN2W7ujxshpGka/b5sPp4dTR5me12XHUwMDFmO8xcdTAwMTCnwWf5rcjvr5+Pflx1MDAxOFx1MDAwZvtneXWcg/qxL1x1MDAxZualx6vH/1x1MDAwMIt/SH0ifQ==

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Booking

Exchange

Cancellation

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nI1WwXLbNlx1MDAxML37KzTqNUWw2Fx1MDAwNbDIrU56yKXTiafTznRyoCVaYkyTXHUwMDFhkorjZvzvfaRcdTAwMTRRVO1hdbAlLPbh7e7bXHUwMDA1vl8tXHUwMDE2y+5ply/fLZb5t1VWXHUwMDE26yZ7XFy+6de/5k1b1Fx1MDAxNUxu+N3W+2Y17Nx23a599/bt6GFW9cPBKy/zh7zqWuz7XHUwMDFiv1x1MDAxN4vvw19YinXv++U6v/3rhp++fGhcdTAwMWWLrdebT9lcdTAwMWafXHUwMDA212HTXHUwMDBmMmVR5ePqNyxcdKlcdTAwMDajkb2N1obAJ+tcdTAwMTOs5JWcIbXsOJKLdLI+XHUwMDE2627b74CT8YnJJ1x1MDAxN9W7OFx1MDAwMmzzYrPtsIdZjWhyNqlcdTAwMGJqddySVZuyJ2ZPK23X1Pf5+7qsm57wT7llYlx1MDAxYTnfZqv7TVPvq/Xre+6KsrzpnspDVrPVdt+cRX044c8jf3exfvJra9Rg9MKRm22Vt+2Ebb3LVkX3dMjDabXnt/u4XHUwMDFlivV5ZNVkXHUwMDBm+ce+WtW+LE/LRbXO+0Isb5t/JsdV6+NxP0o91tFcdTAwMWRXnkfyed4jqziHMvrT+qg2ZuHL5d/qapBcdTAwMWVFXHUwMDE3nNiY4kir/Vx1MDAwMM11XHUwMDAz6l1WtvlYgZ7ar2d6XHUwMDFjQ9zv1tnBhaKoaFRmXHUwMDEyPdkhv/vL+Mt6df/CKbu6OJd7/1x1MDAxOb8txlx1MDAxMlxmP07fP795cTcn41x1MDAxMrlAwslcdTAwMDeb6Nz751x1MDAxNExcdTAwMDBdXHUwMDBiXHUwMDBiTEFkXHUwMDBlTtSQ2OCdXHUwMDE3QKaY/Fx1MDAwNI9cIlx1MDAxOU1cdTAwMTLYojc0OuU5QLJkXHUwMDFjh5RcdTAwMTKpXHUwMDBmnMZKXHUwMDFkXHUwMDAwVYxccjjNxYhcbkeaw3PEhskpXHUwMDFhLiiopilcXFCTlNE0SaOQ1TibP8smhKDOXCJPkdVdXHUwMDA0TN5cYsRcdTAwMTMoWsgopFk8TSaJsorlmJyfhitkOFx1MDAwNMJHJUbHs+FKSIbYXHUwMDA2TFx1MDAxNlEl0JjiReNcdTAwMTGu9ZpQsjRcdTAwMWKtXHUwMDE3b5R9XHUwMDEy21x1MDAwZj6+UEtU45BSdk6QW/WzxVxyYqGvXHUwMDFlXHUwMDA3XHRKYDHNnfeGbITNOzRcdTAwMGJYzuEh0YaVI/6z+kiXWkHpUVxuXHUwMDFjhFlA3oY5PCRcdTAwMDXzXHUwMDFmiUNcdTAwMDKtI1xyUzwnJoq3ySNcdTAwMWKqmPPzYn7tRjhcdTAwMTI0PnhcdTAwMTboXFyciEuzgFx0/YRcdTAwMWVTSdhcdTAwMWODm+DBhk5cdTAwMWKaQygw6SxcdTAwMWF6jZxVSlx1MDAxMTknL+do5PpqeGLvNNggaVx1MDAxNi7GXGKPhEiQoVx1MDAwNDVP4LxcdTAwMThx6Jxoe7WTm9WKS9AqWSRcXFx1MDAxMqWpkMHaQFwiXlLCuKZ55Xmrhq2mZK2ilZgncFxi1VqBjokkoG/jLFx1MDAxZatFYYODUFx1MDAwM0fnJ0pxUCVcbqtOUt+EXHUwMDFh/8dUXHSYQ76XXHUwMDAxcbQ8gcNRuNQtQSr90JFZco7xUFx0jOyhXHUwMDBlooh9mjs1XHUwMDAxQmFFNTxcdTAwMDbz7FBBq2JcbkSrIIdcdTAwMTGP6+NcdTAwMWNcdTAwMGZ3XHUwMDA2Olxml6Z6j7mi811BmFx1MDAwMpg+LkFZYFx1MDAxMya16EfEyI7mb4zX7r+rXHUwMDBir2WZtd37+uGh6HAx/95fqpdcdTAwMTdw22VNd41nSFFtLm15tX7FMnj90jT14zbP/vOogd+rtl1dPm2GJ0nX7PNh9fnqSHmZ7XY3XHUwMDFk3lx1MDAxMKdnz/JrkT9ev/Dou1x1MDAxYj7Lq+MrqH/05cNr6fnq+V/8q0SQIn0=

POST-BOOKING

CRUISING

Fragmented Model

POST-BOOKING

CRUISING

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Booking

Exchange

Cancellation

Selection

Booking

Search

Exchange

Cancellation

POST-BOOKING

CRUISING

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Booking

Exchange

Cancellation

Selection

Booking

Search

Exchange

Cancellation

Text

works around

Sclerosed
Model

Outgrowth
Model

Sclerosed
Model

Outgrowth
Model

works around

Distributed
Model

becomes a fragment of

is a fragment of

causes

Extrinsic
Coupling

Business
Logic
Leak

favorises

induces

exacerbates

is a form of

Extrinsic
Coupling

Outgrowth
Model

Model
Fragmentation

Model fragmentation (extrinsic coupling):
the insidious builder of distributed BBOMs

Model
Evolutions

Model
Fragmentation

Extrinsic
Coupling

is a form of

distributes

Synced
Failures

may induce

Fragmented
Model
Fragility

Synced
Failures

Fragmented
Model
Rigidity

Distributed
BBOM

cause

cause


Model
Evolutions

Distributed

leads to

System
Entropy

raises

binds the fragments into

entangles

into

Exploring (extrinsic)
model tensions heuristics

???

Business Tension

Business Capability Discrepancy

Business Tension

Business Capability Discrepancy

A capability varies in scope, maturity, or behavior across the system, causing gaps and inconsistencies.

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

Booking

Exchange

Cancellation

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nI1WwXLbNlx1MDAxML37KzTqNUWw2Fx1MDAwNbDIrU56yKXTiafTznRyoCVaYkyTXHUwMDFhkorjZvzvfaRcdTAwMTRRVO1hdbAlLPbh7e7bXHUwMDA1vl8tXHUwMDE2y+5ply/fLZb5t1VWXHUwMDE26yZ7XFy+6de/5k1b1Fx1MDAxNUxu+N3W+2Y17Nx23a599/bt6GFW9cPBKy/zh7zqWuz7XHUwMDFiv1x1MDAxN4vvw19YinXv++U6v/3rhp++fGhcdTAwMWWLrdebT9lcdTAwMWafXHUwMDA212HTXHUwMDBmMmVR5ePqNyxcdKlcdTAwMDajkb2N1obAJ+tcdTAwMTOs5JWcIbXsOJKLdLI+XHUwMDE2627b74CT8YnJJ1x1MDAxN9W7OFx1MDAwMmzzYrPtsIdZjWhyNqlcdTAwMGJqddySVZuyJ2ZPK23X1Pf5+7qsm57wT7llYlx1MDAxYTnfZqv7TVPvq/Xre+6KsrzpnspDVrPVdt+cRX044c8jf3exfvJra9Rg9MKRm22Vt+2Ebb3LVkX3dMjDabXnt/u4XHUwMDFlivV5ZNVkXHUwMDBm+ce+WtW+LE/LRbXO+0Isb5t/JsdV6+NxP0o91tFcdTAwMWRXnkfyed4jqziHMvrT+qg2ZuHL5d/qapBcdTAwMWVFXHUwMDE3nNiY4kir/Vx1MDAwMM11XHUwMDAz6l1WtvlYgZ7ar2d6XHUwMDFjQ9zv1tnBhaKoaFRmXHUwMDEyPdkhv/vL+Mt6df/CKbu6OJd7/1x1MDAxOb8txlx1MDAxMlxmP07fP795cTcn41x1MDAxMrlAwslcdTAwMDeb6Nz751x1MDAxNExcdTAwMDBdXHUwMDBiXHUwMDBiTEFkXHUwMDBlTtSQ2OCdXHUwMDE3QKaY/Fx1MDAwNI9cIlx1MDAxOU1cdTAwMTLYojc0OuU5QLJkXHUwMDFjh5RcdTAwMTKpXHUwMDBmnMZKXHUwMDFkXHUwMDAwVYxccjjNxYhcbkeaw3PEhskpXHUwMDFhLiiopilcXFCTlNE0SaOQ1TibP8smhKDOXCJPkdVdXHUwMDA0TN5cYsRcdTAwMTMoWsgopFk8TSaJsorlmJyfhitkOFx1MDAwNMJHJUbHs+FKSIbYXHUwMDA2TFx1MDAxNlEl0JjiReNcdTAwMTGu9ZpQsjRcdTAwMWKtXHUwMDE3b5R9XHUwMDEy21x1MDAwZj6+UEtU45BSdk6QW/WzxVxyYqGvXHUwMDFlXHUwMDA3XHRKYDHNnfeGbITNOzRcdTAwMGJYzuEh0YaVI/6z+kiXWkHpUVxuXHUwMDFjhFlA3oY5PCRcdTAwMDXzXHUwMDFmiUNcdTAwMDKtI1xyUzwnJoq3ySNcdTAwMWKqmPPzYn7tRjhcdTAwMTI0PnhcdTAwMTboXFyciEuzgFx0/YRcdTAwMWVTSdhcdTAwMWODm+DBhk5cdTAwMWKaQygw6SxcdTAwMWF6jZxVSlx1MDAxMTknL+do5PpqeGLvNNggaVx1MDAxNi7GXGKPhEiQoVx1MDAwNDVP4LxcdTAwMThx6Jxoe7WTm9WKS9AqWSRcXFx1MDAxMqWpkMHaQFwiXlLCuKZ55Xmrhq2mZK2ilZgncFxi1VqBjokkoG/jLFx1MDAxZatFYYODUFx1MDAwM0fnJ0pxUCVcbqtOUt+EXHUwMDFh/8dUXHSYQ76XXHUwMDAxcbQ8gcNRuNQtQSr90JFZco7xUFx0jOyhXHUwMDBlooh9mjs1XHUwMDAxQmFFNTxcdTAwMDbz7FBBq2JcbkSrIIdcdTAwMTGP6+NcdTAwMWNcdTAwMGZ3XHUwMDA2Olxml6Z6j7mi811BmFx1MDAwMpg+LkFZYFx1MDAxMya16EfEyI7mb4zX7r+rXHUwMDBir2WZtd37+uGh6HAx/95fqpdcdTAwMTdw22VNd41nSFFtLm15tX7FMnj90jT14zbP/vOogd+rtl1dPm2GJ0nX7PNh9fnqSHmZ7XY3XHUwMDFk3lx1MDAxMKdnz/JrkT9ev/Dou1x1MDAxYj7Lq+MrqH/05cNr6fnq+V/8q0SQIn0=

POST-BOOKING

CRUISING

Fragmented Model

Booking Lifecycle

Created

Completed

Exchanged

Cancelled

POST-BOOKING

CRUISING

chooseSeatPreferences()

Architectural Tension

Fragmented Lifecycle

When separate bounded-contexts handle different states of the same aggregate, while sharing many concepts, personae, and rules.

Architectural Tension

Fragmented Lifecycle

POST-BOOKING

CRUISING

MOBILE APP

Booking

Exchange

Cancellation

Determining current booking status

check booking exists

check if exchange or cancellation exists

Created

Completed

Exchanged

Cancelled

POST-BOOKING

CRUISING

MOBILE APP

check if exchanged/cancelled

check booking exists

Determining current booking status

BOOKING

Architectural Tension

Distributed States

Understanding the state of a concept requires consolidating data from multiple bounded-contexts

Architectural Tension

Distributed States

noDepartureInThePast()

noDepartureInThePast()

noScheduleOverlap()

noScheduleOverlap()

POST-BOOKING

CRUISING

Selection

Booking

Search

Exchange

Cancellation

Architectural Tension

Business Logic Leaks

Behaviors, invariants, lifecycles, etc. are scattered across bounded-contexts

Architectural Tension

Business Logic Leaks

POST-BOOKING

CRUISING

New rule for enabling them to a user

Ancillaries

Ancillaries

Architectural Tension

Frequent
Breaking Changes

Every model evolution breaks collaborating bounded-contexts.

Architectural Tension

Frequent
Breaking Changes

POST-BOOKING

CRUISING

Booking

Release 463

Release 52

POST-BOOKING

CRUISING

ignores

Release 464

Release 52

Booking

Insurance
Policy

Insurance
Policy

Cancellation

POST-BOOKING

CRUISING

ignores

Release 464

Release 52

Booking

Insurance
Policy

Insurance
Policy

Incompatible

Cancellation

POST-BOOKING

CRUISING

Insurance
Policy

Release 464

Release 53

Deploy Together

Booking

Insurance
Policy

Insurance
Policy

Cancellation

Process Tension

Synchronized Releases

Necessity to synchronize heavily around releases and model evolutions across systems

Process Tension

Synchronized Releases

POST-BOOKING

CRUISING

Release 465

Release 57

Incompatible

Special Meal

POST-BOOKING

CRUISING

Release 465

Release 57

Compatible

Toggle: OFF

Special Meal

POST-BOOKING

CRUISING

Release 465

Release 58

Compatible

Toggle: OFF

Toggle: OFF

Special Meal

Special Meal

POST-BOOKING

CRUISING

Release 465

Release 58

Toggle: ON

Toggle: ON

Special Meal

Special Meal

Compatible

Process Tension

Compensatory Countermeasures

Unresolved design problems are compensated by processes such as multi-versioning, double run, feature flags to limit impact on another team

Process Tension

Compensatory Countermeasures

POST-BOOKING

CRUISING

Special Meal

Interface

POST-BOOKING

CRUISING

Special Meal

Add Support

Special Meal

Interface

Organizational Tension

Contract

Over-Negotiation

Interfaces between systems are heavily negotiated by teams

Organizational Tension

Contract

Over-Negotiation

TODo

Current Quest

Done

JIRA

???

Add a

Konami Code

Bug

Bug

Special meals

cancellation

Bug

Handle cancellation insurance

Overlapping ships can be selected

Deprecated seat preference shown during exchange

Bug

Feature

Feature

[Paid Option] Wireless pidgin connexion

Organizational Tension

Backlog Takeover

When the work of a stream-aligned team is systematically on the critical path of another team

Organizational Tension

Backlog Takeover

Organizational Tension

Inter-Team

Work Coupling

Teams need to constantly synchronize their plans and priorities despite their scope of autonomy

Organizational Tension

Inter-Team Work Coupling

TODo

Current Quest

Done

JIRA

Bug

Overlapping ships can be selected

Deprecated seat preference shown during exchange

Bug

Feature

Feature

[Paid Option] Wireless pidgin connexion

Handle cancellation insurance

Special meals

cancellation

Bug

???

TODo

Current Quest

Done

Inconsistent
special meal

orders

Bug

???

JIRA

Organizational Tension

Unclear

Ownership

Organizational Tension

Unclear Ownership

When a need for feature debugging or development arises, debates often break out to determinate which team should be in charge

Extrinsic coupling signals are mainly socio-technical

Extrinsic
Coupling

Frequent
Breaking
Changes

leads to

Synchronized
Releases

leads to

Extrinsic
Coupling

Synchronized
Releases

Frequent
Breaking
Changes

Feature
Toggles

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

leads to

leads to

buffered by

buffered by

Contract
Over
Negotiation

Extrinsic
Coupling

leads to

leads to

buffered by

buffered by

Feature
Toggles

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

enabling
cross-team activation of

Synchronized
Releases

Frequent
Breaking
Changes

Extrinsic
Coupling

Architectural
Tension

leads to

leads to

buffered by

buffered by

Feature
Toggles

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

enabling
cross-team activation of

Synchronized
Releases

Contract
Over
Negotiation

Extrinsic
Coupling

Process
Tension

Architectural
Tension

leads to

leads to

buffered by

buffered by

Feature
Toggles

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

enabling
cross-team activation of

Contract
Over
Negotiation

Extrinsic
Coupling

Process
Tension

Architectural
Tension

leads to

leads to

buffered by

buffered by

Compensatory
Process

Measures

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

enabling
cross-team activation of

offset

offset

Contract
Over
Negotiation

Compensatory
Organizational
Measures

Extrinsic
Coupling

Process
Tension

Architectural
Tension

leads to

leads to

buffered by

buffered by

Compensatory
Process

Measures

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

offset

offset

offset

Compensatory
Organizational
Measures

Extrinsic
Coupling

Process
Tension

Architectural
Tension

leads to

leads to

buffered by

buffered by

Compensatory
Process

Measures

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

offset

offset

offset

never address

Compensatory
Organizational
Measures

Extrinsic
Coupling

Process
Tension

Architectural
Tension

leads to

leads to

buffered by

buffered by

Compensatory
Process

Measures

Tests Envs Proliferations

Synced
plannings
& backlogs

Feature Flags
Overuse

Multi
Versionning

Shared & Distributed
DataSets

offset

offset

offset

never address

Organizational Entropy

Business

Tensions

Architectural
Tensions

reflects as

Process
Tensions

arise by trying to compensate

produce

Extrinsic
Coupling

Organizational Tensions

structure

produce

Post-Booking Team creation

Model Fragmentation

Structure eats Strategy

Jan Bosch - 2017

B.A.P.O Model

Extrinsic coupling Signals are mainly socio-technical

Intrinsic Coupling

Model Tension
Heuristics

Model
Tension

spot

prevents

Model
Entropy

Extrinsic Coupling

prevents

Model
Tension

Model Tension
Heuristics

spot

System
Entropy

Conclusion

... but are only heuristics. Not surefire rules.

Model Tension Heuristics

prevent accidental design debt

Signals

Cart

SHOPPING

Order

ORDER

Parcel

DELIVERY

...do not always reflect tensions.

This is not a fragmented lifecyle

Look out for signals

In the code

Look out for signals

In the code

In conversations

Look out for signals

In the code

In conversations

In processes

Look out for signals

In the code

In countermeasures

In conversations

In processes

Build a Case

Gather a body of evidences revolving around:

parts of the model

parts of the organization

specific processes

Build a Case

Gather a body of evidences revolving around:

parts of the model

parts of the organization

specific processes

parts of the socio-technical system

Experiment on the system

Once you have a body of evidences, act on the signals by running limited, safe to fail experiments to test your heuristics

POST-BOOKING

CRUISING

Exchange

Selection

Booking

Search

Cancellation

Search

Booking

POST-BOOKING

CRUISING

Exchange

Selection

Booking

Search

Cancellation

Search

Booking

POST-BOOKING

CRUISING

Booking

Exchange

Selection

Booking

Search

Cancellation

Search

Shared
Kernel

Model Mitosis

POST-BOOKING

CRUISING

Booking

Exchange

Selection

Booking

Search

Cancellation

Search

SK

Model Mitosis

POST-BOOKING

CRUISING

Booking

Exchange

Selection

Booking

Search

Cancellation

Search

Model Mitosis

BOOKING

CRUISING

Booking

Exchange

Selection

Booking

Search

Cancellation

Search

SHODO

Model Tension
Heuristics

Preventing Accidental Design Debt

Julien Topçu

Josian Chevalier

Tech Coach

CTO / Coach

@julientopcu.com

‪@josianchevalier.bsky.social‬

Model Tension Heuristics: Preventing Accidental Design Debt

By Julien Topçu

Model Tension Heuristics: Preventing Accidental Design Debt

”All models are wrong, but some are useful.” This insight from George Box has become a cornerstone of software modeling. While exploration and experimentation are essential for shaping models, it is often challenging to know when a model is wrong or, worse, when an existing model has become obsolete and is no longer as useful. As a result, unsuitable models may be still used for a long time, leading to different issues not always identified as forms of design debt. These include the spork effect (intrinsic coupling), model fragmentation (extrinsic coupling), and model sclerosis, which ultimately contribute to entropy or the "Big Ball of Mud" phenomenon. These problems arise from undetected and unresolved model tensions. In this talk, we will explore what model tensions are, the complications they cause and why they occur. We will also present a set of heuristics to identify and address different kind of model tensions.

  • 280