Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mise en Place for Ecto

Mise en Place for Ecto

Just as a chef organizes ingredients before cooking—mise en place—successful Elixir applications require organizing domain complexity before implementation. This talk introduces Business Rule Thinking, a disciplined approach to modeling rich domains in Ecto that goes far beyond simple belongs_to and has_many associations.

Drawing from the Business Rules Manifesto and domain modeling principles, you'll learn three essential abilities:

1. Model the Domain as Facts: Using the 7W Framework, we'll discover entities and relationships in your business domain, creating a Fact Model that captures concrete, verifiable statements about business activity—separate from implementation concerns.

2. Derive Business Rules from Facts: Learn to identify the five types of business rules that constrain relationships between entities, moving beyond process-focused thinking to rule-based domain design.

3. Transform Business Rules into Association Validations: Discover practical patterns for implementing business rules as association validations in Ecto, including the association changeset pattern and validation pipelines that ensure your domain logic remains explicit and maintainable.

Key takeaways for the audience

- Move beyond simple CRUD to express rich business concepts in Ecto
- Structure domain logic that collaborates naturally as complexity grows
- Build flexibility for evolving business requirements
- Implement validation patterns that make business rules explicit and testable

Target audience

Perfect for Elixir developers who want to elevate their domain modeling beyond basic associations and create applications that truly reflect business reality.

Avatar for Nicholas Henry

Nicholas Henry

August 28, 2025
Tweet

More Decks by Nicholas Henry

Other Decks in Programming

Transcript

  1. From the book Domain Storytelling “ A good domain model

    prevents programmers (and users) from breaking the rules of the domain.”
  2. Har v e s t Tra c k e r

    Connecting restaurants with local farms Notation for Domain Storytelling, https://domainstorytelling.org/
  3. Har v e s t Tra c k e r

    Connecting restaurants with local farms Notation for Domain Storytelling, https://domainstorytelling.org/
  4. Har v e s t Tra c k e r

    Connecting restaurants with local farms Notation for Domain Storytelling, https://domainstorytelling.org/
  5. Har v e s t Tra c k e r

    Connecting restaurants with local farms Notation for Domain Storytelling, https://domainstorytelling.org/
  6. Har v e s t Tra c k e r

    Business Rules • An express order can only fulfill a seasonal contract with a premium service level • An order can only fulfill an active seasonal contract • An order can only fulfill a seasonal contract without pending orders
  7. @nicholasjhenry Mis e e n P l ac e f

    o r Ec t o Organizing Domain Complexity with Business Rule Thinking
  8. Mod e l i n g t h e do

    m a i n fa c t s
  9. Business Rules Manifesto “Rules de fi ne the boundary between

    acceptable and unacceptable business activity.”
  10. Fact Model (aka concept model) • Business visualization • Concrete,

    verifiable statements • Representation of entities and relationships Mod e l i n g t h e do m a i n as a se t o f f a c t s
  11. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  12. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  13. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  14. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  15. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  16. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  17. Read “Business Knowledge Blueprints: Enabling Your Data to Speak the

    Language of the Business ” by Ronald G. Ross, “the father of Business Rules ”
  18. De fi n i n g bu s i n

    e s s r ul e s
  19. • Derived from facts • Five Rule Types • Robust

    Domain • 7W Framework • Fact Model • Verifiable Statements
  20. Fro m b u s i n e s s

    ru l e s t o va l i d a ti o n s
  21. defmodule HarvestTracker do @spec place_order(%{ seasonal_contract_id: 42, # 👀 cast

    the foreign key line_items: [] }) : : {:ok, Order.t()} | {:error, Ecto.Changeset.t(Order.t())} def place_order(attrs) do %Order{} | > Order.changeset(attrs) # 👀 casting and validating params | > Repo.insert() # 👀 data manipulation end end Phoenix Context Module
  22. defmodule HarvestTracker do @spec place_order(%{ seasonal_contract_id: 42, # 👀 cast

    the foreign key line_items: [] }) : : {:ok, Order.t()} | {:error, Ecto.Changeset.t(Order.t())} def place_order(attrs) do %Order{} | > Order.changeset(attrs) # 👀 casting and validating params | > Repo.insert() # 👀 data manipulation end end Phoenix Context Module
  23. defmodule HarvestTracker do @spec place_order(%{ seasonal_contract_id: 42, # 👀 cast

    the foreign key line_items: [] }) : : {:ok, Order.t()} | {:error, Ecto.Changeset.t(Order.t())} def place_order(attrs) do %Order{} | > Order.changeset(attrs) # 👀 casting and validating params | > Repo.insert() # 👀 data manipulation end end Phoenix Context Module
  24. defmodule HarvestTracker do @spec place_order(%{ seasonal_contract_id: 42, # 👀 cast

    the foreign key line_items: [] }) : : {:ok, Order.t()} | {:error, Ecto.Changeset.t(Order.t())} def place_order(attrs) do %Order{} | > Order.changeset(attrs) # 👀 casting and validating params | > Repo.insert() # 👀 data manipulation end end Phoenix Context Module
  25. Validating Associations • What form of association passing? • How

    do I associate records? • Where do I put the association validation rules? Fro m b u s i n e s s ru l e s t o va l i d a ti o n s
  26. # Context Action def place_order(attrs) do seasonal_contract = get_seasonal_contract(attrs["seasonal_contract_id"]) %Order{}

    | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end Option 1: Mapped Foreign Key
  27. # Context Action def place_order(attrs) do seasonal_contract = get_seasonal_contract(attrs["seasonal_contract_id"]) %Order{}

    | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end Option 1: Mapped Foreign Key
  28. # Context Action def place_order(attrs) do seasonal_contract = get_seasonal_contract(attrs["seasonal_contract_id"]) %Order{}

    | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end Option 1: Mapped Foreign Key # 💥
  29. # Controller Action def create(conn, %{ "seasonal_contract_id" = > seasonal_contract_id,

    # 👀 pattern match id "order" = > order_params } = params) do result = HarvestTracker.place_order(seasonal_contract_id, order_params) # . . . end # Context Module defmodule HarvestTracker do def place_order( seasonal_contract_id, # 👀 explicit parameter attrs ) do seasonal_agreement = get_seasonal_contract(seasonal_contract_id) # 👀 fetch assoc struct %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end end Option 2: Foreign Key Param
  30. # Controller Action def create(conn, %{ "seasonal_contract_id" = > seasonal_contract_id,

    # 👀 pattern match id "order" = > order_params } = params) do result = HarvestTracker.place_order(seasonal_contract_id, order_params) # . . . end # Context Module defmodule HarvestTracker do def place_order( seasonal_contract_id, # 👀 explicit parameter attrs ) do seasonal_agreement = get_seasonal_contract(seasonal_contract_id) # 👀 fetch assoc struct %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end end Option 2: Foreign Key Param
  31. # Controller Action def create(conn, %{ "seasonal_contract_id" = > seasonal_contract_id,

    # 👀 pattern match id "order" = > order_params } = params) do result = HarvestTracker.place_order(seasonal_contract_id, order_params) # . . . end # Context Module defmodule HarvestTracker do def place_order( seasonal_contract_id, # 👀 explicit parameter attrs ) do seasonal_agreement = get_seasonal_contract(seasonal_contract_id) # 👀 fetch assoc struct %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end end Option 2: Foreign Key Param
  32. # Controller Action def create(conn, %{ "seasonal_contract_id" = > seasonal_contract_id,

    # 👀 pattern match id "order" = > order_params } = params) do result = HarvestTracker.place_order(seasonal_contract_id, order_params) # . . . end # Context Module defmodule HarvestTracker do def place_order( seasonal_contract_id, # 👀 explicit parameter attrs ) do seasonal_agreement = get_seasonal_contract(seasonal_contract_id) # 👀 fetch assoc struct %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end end Option 2: Foreign Key Param
  33. # Controller Action def create(conn, %{"seasonal_contract_id" = > seasonal_contract_id, "order"

    = > order_params}) do seasonal_contract = HarvestTracker.get_seasonal_contract(seasonal_contract_id) # 👀 fetch struct result = HarvestTracker.place_order(seasonal_contract, order_params) # …. end # Context Action def place_order( %SeasonalContract{} = seasonal_contract, # 👀 passed as a struct attrs ) do %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end Option 3: Struct Param
  34. # Controller Action def create(conn, %{"seasonal_contract_id" = > seasonal_contract_id, "order"

    = > order_params}) do seasonal_contract = HarvestTracker.get_seasonal_contract(seasonal_contract_id) # 👀 fetch struct result = HarvestTracker.place_order(seasonal_contract, order_params) # …. end # Context Action def place_order( %SeasonalContract{} = seasonal_contract, # 👀 passed as a struct attrs ) do %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end Option 3: Struct Param
  35. # Controller Action def create(conn, %{"seasonal_contract_id" = > seasonal_contract_id, "order"

    = > order_params}) do seasonal_contract = HarvestTracker.get_seasonal_contract(seasonal_contract_id) # 👀 fetch struct result = HarvestTracker.place_order(seasonal_contract, order_params) # …. end # Context Action def place_order( %SeasonalContract{} = seasonal_contract, # 👀 passed as a struct attrs ) do %Order{} | > Order.changeset(attrs) # TODO : Validate and associate the struct | > Repo.insert() end Option 3: Struct Param
  36. # build_assoc Ecto.build_assoc(post, :comments) # = > %Comment{id: nil, post_id:

    13} # cast_assoc Ecto.Changeset.cast_assoc( order_changeset, :season_contract, with: &SeasonalContract.changeset/2 ) # put_assoc Ecto.Changeset.put_assoc(order_changeset, seasonal_contract) Associating Structs
  37. C # build_assoc Ecto.build_assoc(post, :comments) # = > %Comment{id: nil,

    post_id: 13} # cast_assoc Ecto.Changeset.cast_assoc( order_changeset, :season_contract, with: &SeasonalContract.changeset/2 ) # put_assoc Ecto.Changeset.put_assoc(order_changeset, seasonal_contract) Associating Structs
  38. # build_assoc Ecto.build_assoc(post, :comments) # = > %Comment{id: nil, post_id:

    13} # cast_assoc Ecto.Changeset.cast_assoc( order_changeset, :season_contract, with: &SeasonalContract.changeset/2 ) # put_assoc Ecto.Changeset.put_assoc(order_changeset, seasonal_contract) Associating Structs
  39. # build_assoc Ecto.build_assoc(post, :comments) # = > %Comment{id: nil, post_id:

    13} # cast_assoc Ecto.Changeset.cast_assoc( order_changeset, :season_contract, with: &SeasonalContract.changeset/2 ) # put_assoc Ecto.Changeset.put_assoc(order_changeset, seasonal_contract) Associating Structs # 🎉
  40. # Context Action def place_order(seasonal_contract, attrs) do %Order{} | >

    Order.changeset(attrs) | > Ecto.Changeset.put_assoc(:seasonal_contract, seasonal_contract) | > SeasonalContract.validate_order() # 👀 parent validates child | > Order.validate_seasonal_contract() # 👀 child validates parent | > Repo.insert() end # Context Action def place_order(seasonal_contract, attrs) do %Order{} | > Order.changeset(attrs) | > Order.put_seasonal_contract_changeset(seasonal_contract) # 👀 refactor | > Repo.insert() end Validating Association
  41. # Context Action def place_order(seasonal_contract, attrs) do %Order{} | >

    Order.changeset(attrs) | > Ecto.Changeset.put_assoc(:seasonal_contract, seasonal_contract) | > SeasonalContract.validate_order() # 👀 parent validates child | > Order.validate_seasonal_contract() # 👀 child validates parent | > Repo.insert() end # Context Action def place_order(seasonal_contract, attrs) do %Order{} | > Order.changeset(attrs) | > Order.put_seasonal_contract_changeset(seasonal_contract) # 👀 refactor | > Repo.insert() end Validating Association
  42. # Context Action def place_order(seasonal_contract, attrs) do %Order{} | >

    Order.changeset(attrs) | > Ecto.Changeset.put_assoc(:seasonal_contract, seasonal_contract) | > SeasonalContract.validate_order() # 👀 parent validates child | > Order.validate_seasonal_contract() # 👀 child validates parent | > Repo.insert() end # Context Action def place_order(seasonal_contract, attrs) do %Order{} | > Order.changeset(attrs) | > Order.put_seasonal_contract_changeset(seasonal_contract) # 👀 refactor | > Repo.insert() end Validating Association
  43. # Context Action def place_order(seasonal_contract, attrs) do %Order{} | >

    Order.changeset(attrs) | > Ecto.Changeset.put_assoc(:seasonal_contract, seasonal_contract) | > SeasonalContract.validate_order() # 👀 parent validates child | > Order.validate_seasonal_contract() # 👀 child validates parent | > Repo.insert() end # Context Action def place_order(seasonal_contract, attrs) do %Order{} | > Order.changeset(attrs) | > Order.put_seasonal_contract_changeset(seasonal_contract) # 👀 refactor | > Repo.insert() end Validating Association
  44. # SeasonalContract def validate_order(order_changeset, seasonal_contract) do order_changeset | > validate_express_order(seasonal_contract)

    # 👀 assoc validation end def validate_express_order(order_changeset, seasonal_contract) do order_type = get_f i eld(order_changeset, :type) if order_type = = :express and seasonal_contract.service_level ! = :premium do # 👀 compare add_error( order_changeset, :business_rule, "An express order can only fulf i ll a seasonal contract with a premium service level" ) else order_changeset end end Business Rules implemented as association validations
  45. C # SeasonalContract def validate_order(order_changeset, seasonal_contract) do order_changeset | >

    validate_express_order(seasonal_contract) # 👀 assoc validation end def validate_express_order(order_changeset, seasonal_contract) do order_type = get_f i eld(order_changeset, :type) if order_type = = :express and seasonal_contract.service_level ! = :premium do # 👀 compare add_error( order_changeset, :business_rule, "An express order can only fulf i ll a seasonal contract with a premium service level" ) else order_changeset end end Business Rules implemented as association validations
  46. C # SeasonalContract def validate_order(order_changeset, seasonal_contract) do order_changeset | >

    validate_express_order(seasonal_contract) # 👀 assoc validation end def validate_express_order(order_changeset, seasonal_contract) do order_type = get_f i eld(order_changeset, :type) if order_type = = :express and seasonal_contract.service_level ! = :premium do # 👀 compare add_error( order_changeset, :business_rule, "An express order can only fulf i ll a seasonal contract with a premium service level" ) else order_changeset end end Business Rules implemented as association validations
  47. C # SeasonalContract def validate_order(order_changeset, seasonal_contract) do order_changeset | >

    validate_express_order(seasonal_contract) # 👀 assoc validation end def validate_express_order(order_changeset, seasonal_contract) do order_type = get_f i eld(order_changeset, :type) if order_type = = :express and seasonal_contract.service_level ! = :premium do # 👀 compare add_error( order_changeset, :business_rule, "An express order can only fulf i ll a seasonal contract with a premium service level" ) else order_changeset end end Business Rules implemented as association validations
  48. # SeasonalContract def validate_order(order_changeset, seasonal_contract) do order_changeset | > validate_order_type(seasonal_contract)

    # 👀 assoc validation end def validate_express_order(order_changeset, seasonal_contract) do order_type = get_f i eld(order_changeset, :type) if order_type = = :express and seasonal_contract.service_level ! = :premium do # 👀 compare assoc f i elds add_error( order_changeset, :business_rule, "An express order can only fulf i ll a seasonal contract with a premium service level" # 👀 business rule ) else order_changeset end end Business Rules implemented as association validations
  49. Business Rules implemented as association validations # Context Action def

    place_order(seasonal_contract, attrs) do %Order{} | > Order.changeset(attrs) | > Order.put_seasonal_contract_changeset(seasonal_contract) # Seasonal Agreement def validate_order(order_changeset, seasonal_contract) do order_changeset | > validate_express_order(seasonal_contract) | > Repo.insert() end end
  50. | > validate_maximum_number_of_orders(seasonal_contract) | > validate_minimum_order_total(seasonal_contract) | > validate_active_seasonal_contract(seasonal_contract) |

    > validate_no_pending_orders(seasonal_contract) Business Rules implemented as association validations # Context Action def place_order(seasonal_contract, attrs) do %Order{} | > Order.changeset(attrs) | > Order.put_seasonal_contract_changeset(seasonal_contract) # Seasonal Agreement def validate_order(order_changeset, seasonal_contract) do order_changeset | > validate_express_order(seasonal_contract) | > Order.put_farm_changeset(seasonal_contract.farm) | > Repo.insert() end end | > Order.put_restaurant_changeset(seasonal_contract.restaurant)
  51. • Struct Param • Association changesets • Validate parent/child •

    Derived from facts • Five Rule Types • Rule-based Domain • 7W Framework • Fact Model • Verifiable Statements Business Rule Thinking
  52. • Struct Param • Association changesets • Validate parent/child •

    Derived from facts • Five Rule Types • Rule-based Domain • 7W Framework • Fact Model • Verifiable Statements Business Rule Thinking
  53. • Struct Param • Association changesets • Validate parent/child •

    Derived from facts • Five Rule Types • Rule-based Domain • 7W Framework • Fact Model • Verifiable Statements Business Rule Thinking
  54. • Struct Param • Association changesets • Validate parent/child •

    Derived from facts • Five Rule Types • Rule-based Domain • 7W Framework • Fact Model • Verifiable Statements Business Rule Thinking