Designing Domains
Designing your application around domains can be difficult at first. This guide will help you get started.
1. Break down the spec into actions
Take your functional spec or visual design and do a brainstorming session. For each screen or page, identify each action the screen allows or requires, for example:
- “List products”
- “View product”
- “Update company profile”
- “Count page views”
- “Export CSV of tracking data”
- “Render chart of page views over time”
Think in terms of verbs, not nouns. Focus on what the app does, not what it is made of.
2. Group the actions into domains
Once you have your complete list of actions in a file or up on a board, start grouping related actions together. These “buckets” of related actions become your domains.
Be careful with your names. Avoid generic, meaningless names like “Admin” for your domains. Instead, use a name that summarizes the feature, for example:
Inventory
- “List products”
- “View product”
Profiles
- “Update company profile”
Reports
- “Count page views”
- “Export CSV of tracking data”
- “Render chart of page views over time”
3. Convert the actions into functions
Next, think about each action and convert it into a function. Describe what arguments it takes and what it returns. This will also force you to think about the types (nouns) that your application needs.
Fill in any functions, types, or domains that you know you’ll need to make the domains you outlined work. In this case, we discovered that we needed a “Companies” domain:
Companies
Types
Company
: name
Actions
create_company(params)
: creates a company
Inventory
Types
Product
: name, sku, quantity in stock, company
Functions
list_products(company_id)
: Returns a list of products for the givencompany_id
.get_product(product_id)
: Returns a product by its ID.
Profiles
Types
Profile
: company_id, address, bio
Functions
create_profile(company_id, params)
: Creates a profile for a given company.get_profile(company_id)
: Gets a profile for a given company.update_profile(profile, params)
: Updates a profile.
Reports
Types:
PageView
: metadata
Functions
page_view_count(company_id)
: returns page view count for a given company.export(:page_views, company_id, :csv)
: Exports CSV of page views for a given companypage_view_series(company_id, start, end)
: Returns a series of page view data that can be plotted to a chart.
4. Add OTP where beneficial
Thanks to Erlang, Elixir has access to an amazing set of OTP conventions and libraries built into the language. At this point, now that you know the public API of your domains, examine each domain to decide where you need OTP features.
!> OTP will limit your deployment options, because your app will be stateful. You’ll need more fine-grained control over your deployed instances, and they will need network access to each other.
Types
Don’t assume that your domain types must be structs or be backed by a database table.
Most domain types can be represented as GenServers, and collections can be represented as Supervision trees. This can be helpful in some cases, though definitely not all.
?> See the excellent talk “Selling Food with Elixir” from ElixirConf 2016 for an example of how this works in the real world, and why you would do it.
?> See the LearnElixir.tv episode “Applications” for an example of how to build a todo list with GenServer.
?> See the Elixir OTP Guide starting at “Agent” for a walkthrough on how to use OTP.
Realtime or Concurrent
Anything that needs to be done in realtime, where multiple subscribers or people are looking at it simultaneously, could probably benefit from GenServer and supervision trees.
Background Tasks
Any cron-like operation that needs to happen on a regular basis can easily be done with a GenServer.
Unreliable 3rd Parties
Whenever you must rely on third parties who are unreliable, your app can benefit from wrapping those third parties with a supervision tree.
State Machines
Use GenServer or gen_statem.
?> Example A: Tracking students to and from school, in realtime.
?> Example B: Multiplayer board game, where each player has a turn.
?> Example C: Bank accounts; depositing/withdrawing money.
5. Add persistence
In contrast with “The Rails Way”, it’s only here at the end that you should think about persistence. Only now, when you have the domains, functions, and OTP supervision tree laid out do you really know what kind of persistence your app needs.
Conclusion
You now have a set of well-scoped domains, with all their types, public functions, and persistence defined. From here, you can move on to:
- Implement the domains
- Layer on a GraphQL API
- Layer on an HTML interface with Phoenix