Self-Containedness with semantically strong types as a design principle

The principle of Self-Containedness in the context of semantically strong types refers to a type system where domain-modeled objects, when interacting with each other, produce new valid objects of the same domain. This results in an internally coherent, stable system that requires less external control and naturally composes itself through its structure, much like the mathematical closure: The resulting domain model is self contained.

Self-Containedness as a driver of Self-Organization

By capturing a central yet often implicit goal in software design, the Self-Containedness design principle promotes building domain models that are self-contained, semantically meaningful and closed under their own operations. In such a complete system, domain objects interact naturally and predictably, exhibiting characteristics of self-organization1 and produce other domain objects without falling back to primitives or relying on external orchestration. This principle also applies to the modeling phase as new types emerge naturally when integrating seamlessly into the domain, thereby maintaining internal coherence. While several established design principles such as Domain-Driven Design2, Tell, Don’t Ask3, Algebraic Data Types4 or Type-Driven Development5 emphasize on (aspects of) Self-Containedness, few make its self-contained scope an explicit focus (while providing powerful foundations for constructing domain models aligned with the Self-Containedness principle).

The principle of Self-Containedness in software architecture and domain modeling refers to the property of a system in which operations on domain objects produce results that are themselves valid domain objects. A complete domain model is internally consistent and semantically self-contained: It encapsulates behavior, enforces its own invariants and minimizes the need for external orchestration. The principle of Self-Containedness fosters self-organization within the model: Complex behavior and interactions emerge naturally from the structure and rules defined by the domain itself, rather than through imperative control structures or ad hoc logic. This leads to systems that are easier to reason about, test and evolve. Closely aligned with practices in Domain-Driven Design, Functional Programming6 or Algebraic Data Types, Self-Containedness emphasizes immutability, value semantics and strong typing. It serves as a guiding principle for designing expressive, composable and resilient models that reflect the integrity and intent of the domain they represent.

While many principles contribute to a solid domain model, Self-Containedness brings a distinct focus: Not just how domain models are structured, but how they behave, thereby ensuring that all interactions stay inside the model, preserving meaning, consistency and composability throughout the system.

What happens when true Self-Containedness is achieved?

When true Self-Containedness is achieved, complex behavior arises organically from simple, well defined building blocks. Logic is no longer scattered across services or utilities but is embedded within the structure of the domain itself. As a result, domain objects interact seamlessly and meaningfully, giving rise to a form of self-organization that would otherwise require explicit, manual orchestration.

  • Complex operations emerge from simple building blocks
  • Logic is implicitly embedded within the structure
  • Domain objects interact naturally and elegantly
  • A form of self-organization develops, which would otherwise require manual orchestration

The Self-Containedness principle is no silver bullet! It is one of the many tools for designing sound domain models. Rigid adherence to any principle rarely leads to appropriate solutions!

Self-Containedness in a billing domain

This example demonstrates the Self-Containedness principle in practice: All domain operations remain within the model, are semantically meaningful and return domain-native types. Currency handling is explicit, safe and encapsulated. There’s no leakage of primitives or duplication of logic and the domain speaks for itself.

Baseline

The Money type represents a semantically rich, immutable value object for handling monetary amounts in a specific currency. It encapsulates both the amount and the currency, along with all relevant operations such as add, subtract, multiply and divide. These operations are type-safe, domain-aware and return new instances of Money, ensuring Self-Containedness within the model.

When applying the principle of Self-Containedness, necessary functionality tends to emerge naturally. In the example above, the CurrencyConverter defines what is required to operate on Money objects with differing currencies while maintaining domain boundaries. It serves as a clear contract for a service responsible for currency conversion supporting Self-Containedness. Importantly, currency conversion should not be part of the Currency or Money domain objects: Do not make them know or do more than what falls within their scope as domain objects (this is subject to the aforementioned design principles such as DDD). By following this principle, we can precisely identify the functionality that must be provided - typically by domain aligned services.

The Money type also includes overloaded methods that accept a CurrencyConverter, allowing arithmetic operations between different currencies to remain inside the domain model (convenience methods). This design eliminates the need for external control logic and promotes clarity, safety and composability within the domain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public record Money(BigDecimal amount, Currency currency) {

    public Money {
        // Basic validation
        if (amount == null || currency == null) {
            throw new IllegalArgumentException("Amount and currency must not be null");
        }
    }

    public Money add(Money other) {
        requireSameCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money add(Money other, CurrencyConverter converter) {
        Money converted = other.convertTo(this.currency, converter);
        return this.add(converted);
    }

    public Money subtract(Money other) {
        requireSameCurrency(other);
        return new Money(this.amount.subtract(other.amount), this.currency);
    }

    public Money subtract(Money other, CurrencyConverter converter) {
        Money converted = other.convertTo(this.currency, converter);
        return this.subtract(converted);
    }

    public Money multiply(int factor) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
    }

    public Money multiply(int factor, Currency target, CurrencyConverter converter) {
        return this.convertTo(target, converter).multiply(factor);
    }

    public Money divide(int divisor) {
        return new Money(this.amount.divide(BigDecimal.valueOf(divisor)), this.currency);
    }

    public Money divide(int divisor, Currency target, CurrencyConverter converter) {
        return this.convertTo(target, converter).divide(divisor);
    }

    public Money convertTo(Currency target, CurrencyConverter converter) {
        if (this.currency.equals(target)) return this;
        BigDecimal rate = converter.getExchangeRate(this.currency, target);
        return new Money(this.amount.multiply(rate), target);
    }

    public static Money zero(Currency currency) {
        return new Money(BigDecimal.ZERO, currency);
    }

    private void requireSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
    }
}

No primitive handling. No external control logic. The objects “understand” each other.

When extending the domain model, ensure that new types align with the principle of Self-Containedness by preserving internal coherence and integrating naturally into the existing structure. If this is not possible, it may indicate that the model is diverging from the actual domain - an ideal moment to consider refactoring. Ask yourself whether the domain is being modeled incorrectly or if separate domains are being unintentionally mixed. The principle of Self-Containedness provides a powerful tool for building domain models that evolve organically and precisely reflect the application’s context (concise, self-contained, semantically aligned) and is inherently well suited to revealing design flaws early.

Building blocks of Self-Containedness

Languages like Haskell and modeling approaches such as Domain-Driven Design (DDD) provide essential building blocks for the Self-Containedness principle, notably Newtypes, Smart Types or Value Objects. These constructs help keep operations and semantics within the domain model, enabling self-contained, coherent behavior and thereby implicitly supporting and encouraging Self-Containedness.

To keep the concept of Self-Containedness approachable and simple, no hard distinctions is drawn between terms like Newtypes, Smart Types, Value Objects, Aggregates or Entities - this is subject to (and has been handled perfectly well by) other principles and paradigms such as Domain-Driven Design, Algebraic Data Types or Functional Programming. Instead, the principle of Self-Containedness focuses on how existing paradigms can help build consistent, type-safe domain models and how Self-Containedness seamlessly integrates with existing paradigms.

Newtypes / Smart Types

In Haskell, newtype is a language feature. In Java, we emulate this with semantically meaningful wrappers around primitives, often modeled as records for immutability and clarity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public record Quantity(int value) {

  public Quantity {
    if (value <= 0) {
      throw new IllegalArgumentException("Quantity must be greater than zero");
    }
  }

  public Quantity add(Quantity other) {
    return new Quantity(this.value + other.value);
  }

  public Quantity subtract(Quantity other) {
    int result = this.value - other.value;
    if (result <= 0) {
      throw new IllegalArgumentException("Resulting quantity must be greater than zero");
    }
    return new Quantity(result);
  }

  public static Quantity of(int value) {
    return new Quantity(value);
  }

  public int asInt() {
    return value;
  }

  // Additional operations: subtract, convertTo, multiply, toString or hashCode...
  }
}

Value Objects

As of DDD Value Objects are immutable, self-validating types that represent a concept in the domain and compare by value. In Java, these are cleanly expressed as records since Java 14.

1
2
3
4
5
6
7
8
9
10
11
public record Money(BigDecimal amount, Currency currency) {

  public Money add(Money other) {
    if (!currency.equals(other.currency)) {
      throw new IllegalArgumentException("Currency mismatch");
    }
    return new Money(amount.add(other.amount), currency);
  }

  // Additional operations: subtract, convertTo, multiply, toString or hashCode...
}

The principle of Self-Containedness integrates naturally with established modeling approaches such as Domain-Driven Design. Rather than introducing something foreign, it builds on existing concepts like Newtypes, Value Objects, Aggregates or Smart Types and the like and gives them a sharper focus: Ensuring that domain logic remains self-contained, semantically coherent and composable. In doing so, Self-Containedness does not replace existing concepts, but rather clarifies and strengthens their purpose in practice.

Addressed Anti-Patterns

Domain models aligning with Self-Containedness address and aim to prevent the common anti-patterns Primitive obsession, Stringly typed code as well as an Anemic domain model:

Anti-Pattern Problem
Primitive obsession Loss of meaning, scattered validation logic
Stringly typed Weak typing, no IDE support, fragile logic
Anemic domain model Objects carry only data, but no logic, thus all domain rules are handled externally

1. Primitive obsession

What is it? […]

Complex domain information is modeled using primitive types (String, int, float, etc.).

Typical Symptoms:

  • Validation logic is scattered across the system
  • The code lacks expressiveness
  • Same primitive type used for different concepts (e.g., String for Email, Name, ID)

Example (bad):

1
2
3
4
5
public class User {
	private String name;
	private String email;
	private String id;
}

Better:

1
2
3
4
5
public class User {
	private Name name;
	private Email email;
	private UserId id;
}

Risks:

  • Field confusion
  • No domain logic encapsulated in types
  • Loss of semantic clarity

2. Stringly typed code

What is it? […]

Using String to represent structured, typed information (e.g., statuses, actions, IDs).

Typical Symptoms:

  • Comparing with raw strings ("ACTIVE", "DONE", "START")
  • No IDE auto-completion or refactoring support
  • Fragile and error-prone code

Example (bad):

1
2
3
void handle(String action) {
	if (action.equals("START")) { ... }
}

Better:

1
2
3
4
5
6
7
enum Action { START, STOP, PAUSE }

void handle(Action action) {
	switch (action) {
		case START -> ...
	}
}

Risks:

  • High error potential through typos
  • Code is difficult to search or safely refactor
  • No compiler guarantees

3. Anemic domain model

What is it? […]

Objects carry only data, but no logic, thus all domain rules are handled externally.

Example:

1
2
3
4
public class Invoice {
	List<Item> items;
	BigDecimal total; // calculated externally
}

Better:

1
2
3
4
public class Invoice {
	List<Item> items;
	public Money calculateTotal() { ... }
}

Risks:

  • Violation of object encapsulation
  • Domain objects do not “think” for themselves
  • Increased complexity in service layers

Conclusion

The principle of Self-Containedness ensures not only consistency and type safety - it lays the foundation for powerful, self-organizing systems that elegantly extend and compose themselves. Once applied, you create a domain model that is not just correct but natural, intuitively extending itself and minimizing the need for external orchestration.

Several key properties characterize the principle of Self-Containedness: Completeness ensures that operations return objects of the same domain, keeping interactions within the model. Encapsulation means that validation and behavior are intrinsic to the types themselves, rather than scattered externally. Type consistency avoids fallback to primitive types, maintaining semantic clarity and type safety. This foundation supports self-organization, where complex behavior emerges naturally from the interactions of well defined domain objects. Finally, composability allows types to be safely combined and extended, enabling expressive and scalable domain models.

Glossary

Term Meaning
Algebraic completeness Mathematical operations within a set return elements of the same set
Anemic model Objects that carry data but no behavior
Composability Types can be safely composed and extended
Encapsulation Validation and behavior are intrinsic to types
Primitive obsession Overuse of primitives for complex domain concepts
Self-Containedness A system consistently operates within itself (e.g., Money + Money → Money)
Self-Organization Evolving complex behavior naturally through simple internal rules
Smart type / Newtype A semantically meaningful wrapper around a primitive value
Stringly typed Using strings to represent structured data (e.g., statuses, types, IDs)
Type Consistency No fallback to primitive types
Type-Driven design Architecture based around strong, meaningful types
Value object Immutable object with value equality and encapsulated behavior
  1. Self-Organization in systems theory describes how local rules lead to complex, stable structures without central control. Self-Containedness mirrors this: by tightly defining local domain rules, we enable larger domain behaviors to emerge without external orchestration. 

  2. Domain-Driven Design (DDD) advocates for rich domain models where behavior and data live together. Self-Containedness extends this by emphasizing that domain operations should not just exist but should close - that every interaction remains entirely within the domain. 

  3. Tell, Don’t Ask encourages objects to encapsulate behavior rather than exposing raw data for external logic. Self-Containedness aligns with this by requiring that behavior remains inside the domain model, but adds that the results of such behaviors must also be domain-native objects, not primitives. 

  4. Algebraic Data Types (ADTs) from functional programming guarantee that operations stay within well defined structures. Self-Containedness inherits this ideal but focuses on semantic modeling - not just syntactic correctness but meaningful interactions among domain concepts. 

  5. Type-Driven Development uses types to encode constraints and guarantee correctness. Self-Containedness shares the goal of eliminating invalid states, but it focuses more specifically on the natural flow and composability of domain interactions. 

  6. Functional programming emphasizes composing functions that return domain-specific, strongly typed values, enabling seamless chaining and ensuring that operations remain within the boundaries of the domain, aligning naturally with the principle of Self-Containedness