Structuring Umbrella Apps in Elixir

Adz
5 min readMar 22, 2020

--

Photo by Edu Lauton on Unsplash

I wanted to show the way I’ve been thinking about structuring umbrella apps in Elixir applications. I’ve been using this approach for a while and have found it useful. I have also made a template repo in github to get you started if you want to try it out when building a graphql API in elixir.

I like umbrella apps because they are a nice way to declare and separate internal dependancies. They enforce a one way relationship between apps; one app includes the other, and that’s that. This makes the layers in the app nice and clear, and also allows you to provide your own wrapper and interface around a dependancy, allowing you to easily switch it out later without affecting anything else in the umbrella.

I’ve found there are two kinds of apps I add to my umbrella, apps that provide shared functionality across other apps in the umbrella (I will call them ‘service’ apps for want of a better name), and apps that you might say represent some kind of bounded context (I will call them domain apps).

Broadly there are two approaches I think about: umbrella-as-monolith and umbrella-as-monorepo. The two aren’t necessarily mutually exclusive, you could feasibly switch between the two.

Let’s imagine we want to build a graphql API for an online shop and look at each approach in turn.

Umbrella-As-Monolith

This is the way I would think about structuring an umbrella app if I were sure I wanted to keep all of it’s domain / business logic in one place:

├── README.md
├── apps
│ ├── graphql
│ └── web_server
│ └── db
│ └── domain
├── config
│ └── config.exs
├── mix.exs
└── mix.lock

In reality I probably wouldn’t call domain domain. If the app was for a shop called “Pickles for less” I might do this:

├── README.md
├── apps
│ ├── graphql
│ └── web_server
│ └── db
│ └── pickels_for_less
├── config
│ └── config.exs
├── mix.exs
└── mix.lock

The key point is that pickles_for_less would contain all of the domain and business logic in the app, and the rest of the apps in the umbrella become services for that business logic. If we imagine the life cycle of a user interaction it would do something like this:

Request from the front end -> WebServer -> Graphql -> PicklesForLess
-> Db -> PicklesForLess -> Graphql -> WebServer -> Front End.

Now imagine when someone places an order we send an email. I would add an email app into the umbrella.

├── README.md
├── apps
│ ├── graphql
│ └── web_server
│ └── db
│ └── email
│ └── pickels_for_less
├── config
│ └── config.exs
├── mix.exs
└── mix.lock

This app would wrap the particular email library I was using at that time and could be included into the pickles_for_less app. This is great because it means 1. I could change the email library later without affecting the rest of the app and 2. I now have a nice clear interface to mock should I wish to.

Again a user request could come into the web_server, be routed to graphql, then the pickles_for_less app would do the necessaries, calling out to any other app it may need like the db and email to fulfil the request.

What would the domain actually look like? I’ve seen two approaches, one might be to enforce that the domain is made entirely of pure functions. In this world apps like graphql might have to communicate with the db to get all the data a request needs, hand that into the domain and have it perform validations / transformations / aggregations on it. The other is to just include the various services into pickels_for_less, have it reach out to apps it needs like the db or the email app when it needs to. Again it depends on your app.

Pros

All domain logic in one place, cleanly separated from other services, and that separation is enforced by the compiler.

Each of the services hides its implementation details from the domain, meaning they can be changed without affecting anything else.

This approach may also be good if you aren’t quite sure what the domain is or will be, perhaps with an eye to pulling out a separate domain apps later.

Cons

All domain logic is in one place. This could become unwieldy if the domain is large and the app does a lot.

Do you really have to wrap every dependancy? Is that overkill?

Umbrella-As-Monorepo

In this world I might structure an app something like this:

├── README.md
├── apps
│ ├── graphql
│ └── web_server
│ └── account_sign_up
│ └── orders
│ └── refunds
├── config
│ └── config.exs
├── mix.exs
└── mix.lock

Here our ‘domain’ apps are account_sign_up, orders and refunds . We have a choice here as to whether each app has its own database (if it needs it) or if there is a shared db app that all of them can use. There are pros and cons to each. If you has one eye towards making them separate microservices later in life, then a database each would make sense (if the domain app needed one). It would also mean you could have different kinds of databases in each app, again if it made sense.

In this world we might have many ‘service’ apps which get included in different domain apps. You could have apps as small as a decimal utils app, right up to a pub sub wrapper. Any app that needed to use any of those services could just include them.

Pros

Each domain app can easily be pulled out into a microservice (at least in theory).

Deleting a domain app is very easy, as is understanding a domain app’s responsibility.

You can easily get a good understanding of the umbrellas functionality by glancing at the apps folder.

Services can be shared across apps easily. For example, if two domain apps wanted to send emails, we can add an email app easily and away you go. Then if a third needs emails it’s as easy as including email in that third domain app.

You could build separate releases for different domain apps and may be able to deploy them separately.

Cons

It can be hard to determine what constitutes a domain app. How small a focus should it have? What if those boundaries change? This is the hard problem with miscroservices in general and not unique to this approach.

It might be difficult to reason about how the different domain apps interact with each other. Would refunds need to talk to orders? Should it? Do we need another layer above them both that orchestrates that interaction? Could that happen in the graphql app?

I hope this has helped stimulate some thinking on what an app in an umbrella can really be. Happy coding!

--

--

No responses yet