Lonestar ElixirConf 2017 - KEYNOTE: Phoenix 1.3 by Chris McCord

I don’t think there’s one good answer for this - a lot depends on application. For example, I would imagine that in any real setup you’d need some sort of audit log over order changes - it would look different for update done by the client and one by the support team.

There are much more things like that, that make discussing virtual examples tricky, as was already noted.

1 Like

I do like so much about the above presentation.

The bounded contexts are also good candidates to extract to umbrella applications once they grow too large. I like how you force people to think clearly about those issues.

@chrismccord makes a good point about Ecto being two libraries at this point too. In fact, I would say 3 libraries could be extracted from the code base:

  1. data casting and validation (changesets, cast, embedded_schema, validations)
  2. database access (schema, repo, multi, query dsl)
  3. database migrations API

I had a look at extracting 3) in the past, it did not look too difficult to do so, although I did not hear the need to do so form other people so I did not proceed.

2 Likes

Rather than extracting shared validation logic to a third module, would it be a better idea to simply expose validation functions as part of the boundary? Let’s just use the User as an example. Say you have the following (simplistic) schema:

defmodule App.Users.User do
  # ... imports and such

  schema "users" do
    field :name, :string
    field :username, :string
  end

  def validate_name(%Ecto.Changeset{} = changeset, field, options \\ [])
      when is_atom(field) and is_list(options) do
    changeset
    |> validate_required(field)
    |> validate_length(field, max: Keyword.get(options, :max_length, 100))
  end

  def validate_username(%Ecto.Changeset{} = changeset, field, options \\ [])
      when is_atom(field) and is_list(options) do
    default_format = ~r/\A[a-zA-Z0-9_]+\z/

    changeset
    |> validate_required(field)
    |> validate_length(field, max: Keyword.get(options, :max_length, 32))
    |> validate_format(field, Keyword.get(options, :format, default_format)
  end

  # ... more functions
end

Then inside the App.Users boundary, you could have something along the lines of the following:

defmodule App.Users do
  # ... imports and such
  alias App.Users.User

  def validate_name(%Ecto.Changeset{} = changeset, field, options \\ [])
      when is_atom(field) and is_list(options) do
    User.validate_name(changeset, field, options)
  end

  def validate_username(%Ecto.Changeset{} = changeset, field, options \\ [])
      when is_atom(field) and is_list(options) do
    User.validate_username(changeset, field, options)
  end

  # ... more functions
end

I understand in this example I have created a third module with the validations in it, but let’s just assume that the Users boundary existed to begin with for the base user functionality, such as registration. By exposing these validation functions, you now have the ability to access the “core” user validation without having to duplicate the code. For example, say you had a Support.User, you could have something along the lines of the following for its changeset function:

defmodule App.Support.User do
  # ... imports and such

  def changeset(params \\ %{}) when is_map(params) do
    %App.Support.User{}
    |> cast(params, @required_fields)
    |> App.User.validate_username(:username)
    # ... more local validation 
  end

  # ... more functions
end

I don’t know whether or not this would be considered good practice or not. What are your thoughts?

I’m still trying to wrap my head around the concept of contexts and how to separate and group different parts of the application. For example, in the case above, I had a Users context with a User schema. In my head, it seems like there will be quite a few “pure” contexts like that in a project. The similarity of the two names could get a bit confusing.

I’ve referenced this example in the past on this forum, but I’d like to bring it back up now in the context of contexts. José wrote an article on schemaless queries and embedded schemas when Ecto 2.0 was released. In the example, he had an embedded schema that split data between a Profile and an Account.

Now with contexts, I’m trying to think about where the registration would be placed. Would it be in Account or Profile? My first thought would be Account, but then I think that maybe it would just have its own Registration context. Then it would solely be responsible for the registration and that’s it. It’d be a tiny context with only an embedded schema. It’d likely rely entirely upon other contexts to interact with the repo.

For example, the schema file might look something like this (using the idea from above):

defmodule App.Regisration do
  # ... imports and such

  embedded_schema do
    field :email, :string
    field :password, :string
    field :name, :string
    field :username, :string
  end

  def changeset(params \\ %{}) when is_map(params) do
    %App.Registration{}
    |> cast(params, @registration_fields)
    |> App.Account.validate_email(:email)
    |> App.Account.validate_password(:password)
    |> App.Profile.validate_name(:name)
    |> App.Profile.validate_username(:username)
  end

  # ... more functions
end

But in this case, since the Registration context only does this one task, the context and the schema are in the same file. Is that a bad idea? It seems like there might be a few use cases where that applies. If that’s not something you should be doing, would it be smarter to just have the registration be within the Account context?

Then there’s the matter of more complex calls to Repo. Sometimes you have to build multiple large queries and coordinate between contexts to get everything that you might need. If you’re doing that inside of the context files, then I can see the size of the context growing quickly. You could add other files to your context directory that handle each use case. For example, say you have a Profile call that makes several large queries, you could just split that off into a new file.


lib/
├── profiles/
│   ├─ get_user_profile.ex
│   ├─ profiles.ex
│   ├─ ... more files

Then the call inside your profiles context might look something like:

def get_user_profile(username) when is_binary(username) do
  App.Profiles.GetUserProfile.call(username)
end

Then your context will end up being “skinny” with the logic being contained within the relevant file. Again, I don’t know if these are good ideas or not. I’m just trying to get an idea of how this new structure will work. You could end up with your context directory having 20+ files for each use case. Technically, you could take it a step further, and have something like this is the Registration context:

defmodule App.Registration do
  # ... imports and such

  def register_account(params \\ %{}) when is_map(params) do
    App.Registration.RegisterAccount.call(params)
  end

  # ... more functions
end

In this case, the register_account.ex file is being called to handle all the validation and calls to the other contexts/repo. Then there would be no real “logic” within the context file. It would simply be delegating the actions to the correct files, much like a controller. Would that be taking things a step too far?

I know this post is probably a mess, but it’s me just spitballin ideas about the new structure while trying to understand it. I would love to hear thoughts from @josevalim or @chrismccord on this. Thanks to everyone for the help!

Edit: I just thought of this, so maybe I’m slowly starting to learn. Would the Registration context has it’s own Account and Profile schema files that only have the relevent fields for creating each in the database? So maybe something along the lines of:

defmodule App.Registration.Account do
  # ... imports and such

  schema "accounts" do
    field :email, :string
    field :password_hash, :string

    timestamps()
  end

  # ... more functions
end

Then maybe there could be a register_account.ex file that’s the embedded schema that maps to the form from the website. That takes the data and validates it using the methods shown before the edit, but the calls to Repo use the schemas defined within this context directory rather than called out to a different context (e.g. App.Account.create(data)).

Then your directory might look something along the lines of:

lib/
├── registration/
│   ├─ account.ex
│   ├─ profile.ex
│   ├─ register_account.ex
│   ├─ registration.ex
│   ├─ ... more files?

And your context file might look something along the lines of:

defmodule App.Registration do
  # ... imports and such

  def register_account(params \\ %{}) when is_map(params) do
    App.Registration.RegisterAccount.call(params)
  end

  # ... more functions
end

Then inside the register_account.ex file, you validate the schema, hash the password, start the transaction, and insert all the data into the database. Would the register_account.ex file be allowed to access App.Registration.Account or App.Registration.Profile directly or would it need to talk through its own boundary?

I hope I’m not veering off in the wrong direction at this point.

5 Likes

Boy, I sure killed that discussion. Anyone able to comment or give feedback on my thoughts above? I’d hate for all that writing to have gone to waste.

1 Like

I love the direction Phoenix 1.3 and Ecto 2 are heading in. It’s going to be great, a great library :slight_smile:

However I am a bit concerned that the vocabulary from DDD will confuse people coming from that background since it doesn’t really use the same meaning for words like Bounded Context. In my view, overloading the words with different meaning is a unnesseccary. Bounded Context has a pretty precise meaning in DDD, as described by Fowler and Evans: a boundary for the ubiqutous language of the domains in the application. If I understand the presentation, Chris also use the term to group related modules within the same domain. This is what the DDD community normally refer to as aggregatets.

1 Like

tl;dr… :wink:

2 Likes

The truth is this confusion around context has not helped me at all.

1 Like

The truth is confusion doesn’t help anyone at all. :slight_smile:

I’m trying to write an app inspired by @lance’s prag prog book, so I’ve written an Elixir module to handle the domain.
Then I generated a Phoenix 1.3 project added the domain engine as a dependency and added a Context.

Now I’m wondering where I should actually use my domain engine module. It has no business in the controller or context/boundary so the schema file seems like the only logical place to use it. would that be polluting the schema?

Also, I have all these nice modules/structs in my engine. Are they essentially wasted when I use Ecto’s schema/2 function?