Leaking domain objects

Let's begin with defining a simple command object that will interact with your domain. We'll do a good and a bad example right at the beginning to compare them.

#### BAD ####

module Communications
  class SendEmailNotification
    def initialize(email:, notification_id:)
      raise WrongEmailFormat unless email.is_a?(Communications::Email)

      @email = email
      @notification_id = notification_id
    end
  end
end
#### GOOD ####

module Communications
  class SendEmailNotification
    def initialize(email:, notification_id:)
      @email = Email.new(email)
      @notification_id = notification_id
    end
  end
end
  

At first, the difference might look insignificant. The only thing different is that the first command validates whether the object passed through email parameter is an instance of Communications::Email - an internal Value Object of the Communications module that represents an email address. While this looks innocent, and may even look better at first (because we validate whether the passed parameter is really an email) it brings maintenance complexities.

The biggest problem it introduces here is that the external users of the Communications module will be forced to reach into the Communications module and build this Value Object before they can use the command. They will have to rummage around your domain and to use its internal objects. This will make it harder for you to introduce changes as you fit to the Email class. You'll either have to introduce versioning or deprecation, and this will quickly become a nuisance, especially in new domains where you still discover things, you change your objects constantly to represent the business concepts etc. Commands should be more stable and not so much prone to how you handle some business behavior internally.

So! The main lesson here is that it's better to use primitives or some objects specifically created for passing them into the command instead of using the internal objects representing your domain.