Exercise to help us discover all the concepts living in our organization, and our systems
Get everyone in a room and discover the...
- Nouns - entities
- Verbs - events
- Core domain - primary area of focus of the business
- example: AutoMaxx's core domain is car sales
- Supporting domains - subdomains that support the business/plays a role in making the core domain happen
- examples:
- online listings - put the car onto the site
- financial transactions - charge the buyer and pay the seller
- optimization & analytics - metric tracking
- customer support
- examples:
The terms that the business speaks within each domain; adopt them instead of having separate nomenclature within an engineering organization.
- Put these terms into a glossary
- Speak about them consistently within the team
- Use the terms consistently in the code
- Examples:
- A
Rating
has a specific meaning in the inspection subdomain -- vehicle health score- But has a different meaning under customer support subdomain -- customer support experience score
- A
Each domain may use specific nuanced terminology/nomenclature
- keeps our product in sync with the business stakeholders
-
collection & member getters and setters
- domain: Inspection
get_vehicle
list_vehicles
update_vehicle
get_mechanic
-
exposing domain actions/procedures
add_vehicle_to_garage_queue
rate_vehicle
- Discouraged:
- direct usage of a domain entity in another domain
- Suggested:
- conversion (at the boundaries / top-level module) [read-only]
- Struct or model transform
- example:
- from marketing visitor -> marketing user
- In marketing module:
visitor_for_user
function
- In marketing module:
- from marketing visitor -> marketing user
- collaborator schema [read/write]
- avoid cross-context joins
# AVOID THIS defmodule AutoMaxx.Marketing.Visitor do schema "marketing_visitors" do: belongs_to :user, AutoMaxx.Identity.User # DO THIS field :user_id, :string field :user_id, :uuid end
- conversion (at the boundaries / top-level module) [read-only]
Leverage aggregate roots (top-level entities)
-
When we want to work with a vehicle rating or vehicle list price, let's pass around a vehicle instead
- This simplies our APIs (public interfaces)
-
Example:
-
Before:
- Inspection (context) - rating - vehicle - list price
-
After:
- Inspection - vehicle - rating - list price
-
-
Dos / Don'ts
# READING/GETTING ## DON'T defmodule AutoMaxx.Inspection do def get_vehicle, do: # ... def get_vehicle_rating, do: # ... def get_vehicle_list_price, do: # ... end ## DO (should be able to obtain each in aggregate) defmodule AutoMaxx.Inspection do def get_vehicle, do: # ... end # UPDATING / SETTING ## DON'T defmodule AutoMaxx.Inspection do def update_rating(rating_id, new_rating), do: # ... end ## DO defmodule AutoMaxx.Inspection do def rate_vehicle(vehicle_id, new_rating), do: # ... end
Often times we have things like this:
# coupling (why does our identity context need to know about something that is a part of marketing)
# - `subscribe_user_to_email` function
defmodule AutoMaxx.Identity do
def create_user do
do_create_user()
|> AutoMaxx.Inspection.create_mechanic()
|> AutoMaxx.Inspection.subscribe_user_to_email()
|> AutoMaxx.Inspection.track_event("user_created")
end
end
But we should instead just publish an event and interested parties/contexts can subscribe and perform actions/procedures:
## Publish event(s) over a bus
defmodule AutoMaxx.Identity do
def create_user do
do_create_user() |> publish_event("Identity.user.created")
end
use EventBus.EventSource
defp publish_event(%User{} = user, event_name) do
EventSource.notify %{id: UUID.uuid4(), topic: event_name}
%{user: user}
end
end
end
## create EventHandler module in each context
defmodule AutoMaxx.Marketing.EventHandler do
def process({:"identity.user.created" = event_name, event_id}) do
%{data: %{user: user}} = EventBus.fetch_event({event_name, event_id})
AutoMaxx.Marketing.subscribe_user_to_email_list(user)
end
def process({:"another_event, event_id}), do: # ...
end