Understanding contexts, web and DDD

You’ve written quite a detailed post, so forgive me if I skip over anything, but here are my impressions on the scenario you’ve proposed.

I don’t think that an admin panel / back office is a separate domain here. A domain isn’t a subsection of your website. A domain is a “subsection” of your system or business. This relates to what you said:

Routes don’t belong in a domain. Routes belong in your web interface, and your other domains shouldn’t really care about them. Your core business logic domains shouldn’t care if they are being accessed through a web interface, the command line, or a touchscreen kiosk somewhere. I think the separation of your actual application from the web interface is something you’re still missing in your mental model. This talk may be useful.

Neither. The routes belong in a web interface layer, built e.g. with Phoenix, which merely interfaces with the system using its API boundaries. All of the other domains shouldn’t know or care that they are being accessed through the web, and hence they will have no concept of routes.

This ties in with the previous point a bit. Something which I feel many people mistakenly believe about the type of model that is being advocated here is the notion that domains must be completely separate systems that share no data. That is not true. What they should not share is internal implementation details—instead they should communicate through their public interface.

It is true that in the web interface context, the different domains will need access to different types of information about the user. It is true that this information needs to be stored into a session or cookies or some other state by the module that interfaces with the web browser (e.g. Phoenix). However, this does not mean that Phoenix needs to be coupled to these domains.

Instead, it is ok for these domains to return things to be treated as opaque, to be stored and given back later. An example of this might be a User ID. It’s ok for the Phoenix layer to store a User ID somewhere in a session, and provide it to domain modules where needed. It is likewise ok for the Flight Booking domain to hold on to a User ID within its own internal flight booking data. The key here is that these domains never take that ID and directly interface with the database to get user data—instead, they use that ID to ask the Users domain to perform any operations.

Let’s take this particular example. We’re going have three “Domains” here, Authentication, Authorization and Flights, as well as the web interface, Web.

First, if a user logs in, we handle that at the Web layer by passing their credentials to Authentication and obtaining a user_id, which we store in session. Note that the way we get a User struct is by calling the Users domain—we don’t query the database ourselves.

def login(conn, %{"user" => username, "pass" => pass}) do
  case Authentication.auth_with_credentials(username, pass) do
    {:ok, user_id} ->
      conn = put_session(conn, :user_id, user_id)
      user = Users.get_user(user_id)
      render conn, "login_success.html", user: user
    {:error, reason} ->
      render conn, "login_failure.html", reason: reason
  end
end

Now suppose that a request has come in to the Web interface, and we are inside a Phoenix controller, wanting to handle it. The request is for a “view flights” page. To render that page, we obviously need to obtain the flight information. As you said, this information will be different for logged in and non-logged in users, but we don’t care about this at the Web layer, as long as we can render a “hidden” flight (e.g. by showing “sign up now to see this deal!”)

def show_flights(conn, params) do
    # extract the fields we care about, in a format which `Flights` is expecting
    flight_params = sanitize_flight_params(params)

    # we stored user_id in our session if logged in, if not, we use the anonymous user
    user_id = get_session(conn, :user_id) || Authentication.anonymous_user_id()
    flights = Flights.get_flights(params, user_id)
    render conn, "flights.html", flights: flights
end

Note that we don’t care at this layer even whether we are logged in or not. Even if we are, we simply pass the user_id that we were given from Authentication through to Flights. Note that at no point we’ve assumed what user_id actually is—it could be an integer, an atom, a string, and we don’t need to know!

Now, let’s take a look at the Flights domain itself. Its job is to return flights matching certain parameters. But, if a certain user (for example the anonymous user) is not allowed to see premium flights, they should be marked as hidden.

def get_flights(params, user_id) do
  params
  |> fetch_flight_data() # local helper function
  |> Enum.map(fn flight ->
    if premium_flight?(flight) && Authorization.cannot?(user_id, :see_premium_flights) do
      %{flight | hidden: true}
    else
      flight
    end
 end)
end

Again, the Flights domain doesn’t have to care what users actually are or how they work. All it needs is some token for the user that we are returning flights for (user_id) so that it can ask the Authorization domain whether this user is allowed to see premium flights. Likewise, Authorization doesn’t need to know what the :see_premium_flights permission actually means—its only job is to store whether a particular user has that permission or not.

Hopefully that example illustrates what I mean. Also, notice that no domain apart from the web interface actually knows anything about sessions or request. You could easily execute the code inside that controller inside iex and obtain the same results.

Finally, this is only one way to split up the responsibilities across the domains. You may decide that actually Flights shouldn’t know about the fact that some users are not allowed to see premium flights, and instead move that code to the controller itself.

Indeed, if you spread data across two databases, you do have to solve that hard problem. I’m not sure why you would though or what it achieves here. It is ok to add referential integrity checks at the database layer, without coding them into the software layer. Suppose that your Authorization permissions lived in a separate table to main user data, and your user profiles were in a separate domain Users. It’s fine to have a foreign key in the auth table to users, because without a user, their authorizations are meaningless. However, at the software layer, you’ll likely model that field in an Ecto schema as field :user_id, :id instead of belongs_to, :user, Users.User, because the second violates your domain at that layer. As I explained before, it is absolutely fine for domains to be passing around and storing opaque tokens belonging to other domains, as long as they don’t peek inside.

This entire thing is a spectrum and as is often the case, extremes have many problems.

You don’t have one system. You have multiple systems, and you’re trying to integrate them as external systems and wondering why you aren’t getting the benefits of having one system. As I said, usually the web layer is just an interface to the system—it is not a separate domain.

Your CMS and ECommerce domains do not care whether they are being displayed with a live preview or JS libraries. For all they know, they could be called from the command line.

You can achieve different resources being loaded for different sections of the site in other ways, but the beauty of the model we are explaining is that you can make these decisions after the fact. Because your application is completely separate from your web interface, you can have all of the modules be accessible through one huge website, or run a few smaller websites as separate Phoenix apps, all calling into the appropriate domains. You can even have multiple Phoenix “apps” being handled by one Cowboy server with a properly-written plug.

Yes it should.

No, it shouldn’t. It should be able to return a list of products or store pages or categories as a bunch of Elixir data. It’s the job of the web interface to turn that data into HTML that users can see.

Indeed it would be difficult, this is the way that people tend to write apps in Rails and this is exactly what we are arguing against :wink:

Separating your application into domains is again the solution here. If lots of people are searching for flights, the bottleneck could be at your web interface (in which case spin up more nodes with the web interface running), or the bottleneck could be with your flights backend (sorting through and processing all those flights).

Suppose that to date your whole application has been running on a single server, and suddenly you find that it can’t handle the load of processing all those flights. Because all of Flight-related functionality is within a single domain, you can change its internal implementation to actually work with a cluster of nodes behind the scenes to spread out all the work, and as long as you keep the same public interface to Flights, you should be able to get away with little to no code changes anywhere else in your entire application.

The entire point here is decoupling where it makes sense. This includes decoupling access (web) from the system itself.

The default model “encouraged” by Phoenix 1.3 is not strict DDD. It may adopt some concepts, but the main point is Phoenix is not your app.. Build your applications as a bunch of modules and then add a web interface on top. Keep the modules self-contained where practical and feasible. You could do proper DDD in Phoenix, but just following these guidelines gets you really far without the overhead that applying a strict model entails.

9 Likes