3D Type by David McLeod.

DDD within Elixir/Phoenix project: Umbrella apps & Service Object

Andrey Chernykh
5 min readFeb 27, 2017

--

Today I want to talk a bit about Domain-driven Design and Service Object, and how we can follow them within our Elixir/Phoenix projects.

Umbrella apps

Elixir provides the great tool for structuring a project — umbrella apps. An umbrella consists of number of Elixir applications. You can think of them like independent applications. You can refer one application from another, like it is a hex package/library, but placed locally.

Since dependencies between applications under an umbrella are like your regular Phoenix project’s hex-dependencies, you can change one application with another in any moment (while new application offers the desired functionality). I think it’s the main pros of umbrella projects.

Umbrella apps are internal, that means such dependencies are very specific for certain project/series of projects. They can be reused in another project of a company, but likely are not applicable for every Elixir project.

Applications under an umbrella should be as much independent as possible. In well-designed umbrella each app could be either replaced with another or reused in another umbrella.

There are many ways to structure an umbrella project. In common, I split an umbrella apps this way:

  • Core: modules that manage common business logic (logging, auth, various helpers etc.) Core modules are very independent and can be used in different projects.
  • Inventory: modules that describe business entities. Inventory may consist of: DAL (data access layer: ecto schemas, specific repositories, validators etc.), Operations (Exop operations (we will talk about it a bit later)), Services/Workers subdirectories. Some inventories could be used in various projects, as long as an inventory consists of general-purpose entities.
  • Web/API: Phoenix or just Plug-based application. It could be a website or RESTful API. Of course, you can have Web and API at the same time.

Umbrella apps (directories) names are fully up to you. Well, actually, there is no the only one certain nor the best structure. Here is an interesting discussion on ElixirForum, you can find there some inspiration.

Service Object

Since umbrella apps are about the whole project’s structure, it’s parts organisation and decoupling, Service Object is about DDD within one of umbrella internal apps (or a project which is not under an umbrella).

For example, there is no need a controller action to incapsulate business logic (except really tiny ones). It is not a controller responsibility.

Lets’s say you need to implement a Task creation logic in your To-Do application. Here are some requirements:

  • Tasks are placed into Tasks lists
  • Each Task list could be shared with other users (followers)
  • You should notify followers, if a Task was created successfully and a Tasks list has followers

The first approach might be:

There is a lot of code and (what is the worst) TaskController knows too much about business logic of ‘Create a task’ operation.

In this article I use the ‘Operation’ definition, some developers use ‘Service’ definition.

What I don’t like in the code above?

  • action/2 function preloads tasks and followers every request. We should define special function and invoke it in certain actions in order to avoid it. But actions will become more ‘fat’. Definitely, not the ideal approach.
  • create/3 function has high level of nesting (so-called, ‘the pyramid of doom’)
  • create/3 function is fat. Even we’ve moved some logic in separate functions: task_exists?/2 and notify_followers/2
  • we have to duplicate task_exists?/2 and notify_followers/2 in every controller where we should deal we such business logic

Summary: as I said before, TaskController owns/holds business logic. Result: it is really hard to read, maintain, refactor and test such code.

Below I described the way to refactor TaskController

Refactor it!

First of all we should decide: what are responsibilities of TaskController and what is out of that borders.

Duties of a controller are pretty straightforward:

  • accept a request (in the form of Plug.Conn struct)
  • dispatch a response (change the conn of Plug.Conn struct)

The second statement doesn’t refer to business logic source. You can place it into a controller (you shouldn’t), but rather consider the decoupling.

In our case there is no any need to define business logic in the controller. TaskController isn’t responsible for notifying folowers at all. As well as holding the knowledge: when to preload associations, when to check for tasks existence. The controller should know only the place where this kind of logic is stored and ask for it.

For example, we can move all code related to business logic into a separate module named TaskCreateService

Now, after decoupling the controller from logic, we can see the difference. The controller doesn’t bother about actual creation logic, preloads and so on. Service Object decides: whether it should preload associations or not. And we see that TaskCreateService isn’t actually responsible for followers notification too. Moreover, we might need to use notification logic in other services. Let’s define NotificationService and invoke it in TaskCreateService

Now we get thin controller and business logic in separate modules, which we can reuse and test easily.

Exop

We can improve the last approach with little library I’ve written — Exop. This library is just a set of macros that provide convenient DSL for writing ‘operations’. Operation — is a module that incapsulates business logic, defines the invocation endpoint, validates incoming parameters and provides useful features, like parameters coercion or policy check.

I tried to provide on github library’s page as full documentation as possible, so below I just show how we can refactor the code above with Exop.

So, we’ve defined two operations: TaskCreateOperation and NotificationOperation Each operation has its own contract which describes incoming parameters and validations.

Additionally to advantages from Service Object utilizing we’ve got: handy DSL, a contract (set of parameters) validation, the same invocation interface, predictable output and a bunch of features.

For the sake of the example kept things simple and easy to track. In real project it’s much better to store your operations/services in structured directories either in special ‘inventory’ app under an umbrella or in ‘operations’/’service’ directory of a project. In Phoenix project it might be:

# ..web/operations/task/create.exdef module Todo.Operations.Task.Create do  # your operationend

Afterwords

Try to make your project’s components dependencies weak. On high level — applications, on lower levels — entities. Do not overload your entities with extra responsibilities. Decouple. It is much easier to test and maintain such projects. There is no ‘silver bullet’, the only one receipt or tool for DDD adopting. You will get used to the general sense of composition through constant practice.

Hope this article was helpful for you. Happy coding :)

--

--