Ways of slicing umbrella apps

I’ve been reading a lot about structuring Elixir apps, especially with use of umbrella apps, since our codebase is growing and we’re looking for better structure for the code, and more flexibility when it comes to deployment.

I’m especially interested how people are dividing their code into separate umbrella sub-apps.

To illustrate further points let’s imaging we have a system like this:

| Tech component  \     Service -> | Documents | Billing | Authentication | 
| ----------------------------------------------------------------------- |
| web (Plug/Phoenix/GQL)           | x         | x       | x              |
| worker (bg jobs, Exq)            |           | x       | x              |
| sms sender                       | x         |         | x              |
| email sender                     |           | x       |                |
| db (Ecto repo)                   | x         | x       | x              |

Horizontally we have higher level business features/services. Vertically we have low level technicalities, used by one or many services.

I see the following ways to split:

1. split by tech component, have following sub-apps:

  • web - depends on domain, controllers for all system features
  • domain - covers all services: docs/billing/auth, depends on db and queue (for enqueueing jobs)
  • db - Repo, postgrex connection, db migrations, all schemas
  • queue - Redix connection for Exq
  • worker - depends on queue (for pulling jobs), sms-sender and email-sender , handles all jobs for all system features
  • sms-sender
  • email-sender

(domain is not a tech component, but web has to call some business logic somewhere… more on this below)

2. split by feature/service, into independent micro-service-like umbrella sub-apps:

  • documents - runs its own Phoenix endpoint, sms sender and Repo in its supervision tree, has its own Ecto schemas
  • billing - runs its own Phoenix endpoint, email sender, Repo and Exq in its supervision tree, also has its own Ecto schemas, and runs only billing related Exq jobs
  • authentication - runs its own Phoenix endpoint, sms sender, Repo and Exq in its supervision tree, also has its own Ecto schemas, and runs only auth related Exq jobs (you can argue wether authentication is a “feature” of the system, but it definitely could be thought of as (micro)service)
  • entrypoint - Phoenix endpoint depending on above Phoenix apps, combining above endpoint into 1 with multiple Phoenix.Router.forward definitions (or nginx reverse proxy with multiple backends)

(entrypoint is a tech component, not feature of the system, but you need to put them all together somehow at some place… :slight_smile: )

Let’s look at pros/cons of both approaches.

Approach 1.

Makes all stateful components (or ones that perform side effects) a distinct, separate apps. Only domain is stateless (in a sense that domain code is like library, although it depends on other stateful apps here), and is a special case here.

You can create separate Distillery release for let’s say “web release” (web app only, which pulls in domain, db and queue), and “backend release” (worker app only, which pulls in queue, sms-sender, email-sender).

You can easily scale backend separately from the others, but you can’t easily scale only billing background processing - you scale all bg jobs together.

Approach 2.

Makes all system features into independent microservices. You can scale billing independently from everything else (which scales all tech aspects of billing: Phoenix endpoints, Repo connections, Exq workers).

You can create separate Distillery release for each service, and then deploy them separately. This gives ability to easily scale the billing part, without adding unnecessary resources (mem/cpu) to other services which don’t need more. Re-deploying one of the components doesn’t (in theory :wink: ) affect the rest of features.

The cons of this approach: slightly more complicated setup due to not having central place for db migrations and schemas, separate db connection pools require more mem on db server etc. Another thing is that when you want multiple services to use parts of the same data (subset of db table columns), you need to define separate schemas in every service (which can be allright, but is not natural to many folk), and you still need some central place to maintain and apply db migrations.

Approach 3. - everything between 1. and 2? :slight_smile:

I have seen people suggestion something like this:

  • ui
  • api
  • billing
  • accounts
  • db

which mixes both tech aspects and high-level business aspects of the system into equal apps.

I must admit I’m not quite convinced by this, since some of these apps are not really equal, they serve their purpose on different level. db maintains db connection and basically allows you to store and retrieve data, while accounts is more like DDD bounded context.

1, and 2. slice the system in rather specific ways, so they’re conceptually simple (although can be operationally more complex).
The combo here above… it’s all mixed together, so I feel dirty when even thinking about doing this :wink:

I’ve ready probably all topics on the forum regarding umbrellas/context etc.
Maybe someone has some new thoughts on this?

Thanks!

4 Likes

Treat your table over here as an incidence matrix and go with Approach 3 if someone would ask me.

Maybe I am missing the point here but I would follow the general rules of Clean Code:

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

Separate what changes a lot from what doesn’t. Business logic should be a library with no dependencies. Phoenix should not be an integral part of the system (like I see you plan on doing in approach 2) but rather a detail.

Following the scheme on the post I mentioned roughly indicated approach 1 is preferable, although I don’t think anyone understand better your situation then you (you don’t say :stuck_out_tongue: )

1 Like

In the end approach 1 (and general idea behind Clean Architecture) seems to fit my system quite well.
Thanks!

I prefer a something similar to #2 but with a single Endpoint to accept web requests and a shared repo/migrations project.

The main concern for me is that all the code that needs to change when adding a new feature should be in the same app. Ecto Schemas, router, controllers, business logic, tests, etc.

Then each feature oriented app can be wired into the shared web layer with a forward route.

1 Like