Closure is when an operation (such as “adding”) on members of a set (such as “real numbers”) always makes a member of the same set. 1

Closure of Operations was a concept I’ve been lectured on since the elementary school. I always understood the idea since it’s rather a simple concept when you think about it. However, I’ve never given much thought about how it could be useful to me. Learning that the addition on a set of real numbers is closed (ex. given x and y are real numbers, x + y is always a real number) doesn’t add much value on what I do on a daily basis. Then came the day where I encounter the same concept in the Supple Design of a software book: Domain-Driven Design.

The author, Eric Evans, explains the concept from a simplicity point of view. “The property of closure tremendously simplifies the interpretation of an operation, and it is easy to think about chaining together or combining closed operations.”2 Reading this sentence immediately reminded me of method chaining. If there is more than one operation that you need to apply on a given object, you can do so by returning the same object. To achieve the closure of operations, of course, you don’t have to return the same object, but the same type. Still, the method chaining simplifies a chain of operations by eliminating the syntactic sugar which is local variable declarations. A good example of method chaining is the query builders. Many methods of the query builder returns an instance of query builder, so that you can chain them together to build your query:

@employee = Person.joins(:employer).where(age: 32).where(company: "Shopify").order(created_at: :asc).first

While method chaining requires the same instance of object to be returned, the closure of operations is a more generic concept. “A closed operation provides a high-level interface without introducing any dependency on other concepts.”2 That’s why value objects, rather than entities, are better candidates to achieve the closure of operations. Entities are objects that require continuity which is a form of persistence. They require a unique key to differentiate from other entities since we store them (on databases, mostly) and retrieve when we need them. Value objects, on the other hand, live on memory. They are created on demand and then discarded upon fulfilling their purpose. Value objects are best when they’re immutable, because it’s harder to track them.

An example of a value object in Ruby that demonstrates closure of operations is a class for representing a monetary amount.

class MonetaryAmount
  attr_reader :value, :currency

  def initialize(value, currency)
    @value = value
    @currency = currency
  end

  def +(other)
    if @currency != other.currency
      raise "Cannot add amounts with different currencies"
    end
    MonetaryAmount.new(@value + other.value, @currency)
  end
end

Adding any two MonetaryAmount always makes a MonetaryAmount.

wallet1 = MonetaryAmount.new(10, "EUR")
wallet2 = MonetaryAmount.new(32, "EUR")

wallet = wallet1 + wallet2 # The result is another MonetaryAmount instance

Thus, + operation is closed under the members of MonetaryAmount. Although it’s a simple example, MonetaryAmount, being closed, brings no external dependencies. It’s easy to read, test, maintain, and thus extend. The end result is not the same instance that has been modified like the one we achieved with method chaining, but another immutable, standalone MonetaryAmount object. Whenever possible, extracting domain models into value objects can greatly benefit the maintainability of the overall system by lowering the accidental complexity. What we are left with is the essential complexity encapsulated within value objects, entities, and the relations between entities (dependencies).

  1. Pierce, Rod. “Definition of Closure” Math Is Fun. Ed. Rod Pierce. 24 Aug 2018. 23 Jan 2023 http://www.mathsisfun.com/definitions/closure.html 

  2. Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2004.  2