How do you manage umbrella apps with inner dependencies?

Background

I have an umbrella application that has several apps inside. These apps communicate and depend on each other.

So, when I am testing, because I am using mocks, I need to mock their interfaces. Now if you know about mocks or if you have read Jose Valim’s opinion on them (http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/) you know this brings an issue:

  • Using Mocks means I can deviate from the real implementation.

App Structure

Imagine I have an umbrella project with 2 apps:

  • cli (a command line interface)
  • manager (where all the logic is)

Umbrella Project structure

.
├── README.md
├── apps
│   ├── cli
│   └── manager
├── config
│   └── config.exs
├── mix.exs
└── mix.lock

Now, every time I change the manager's Public API, I have to update the interface on cli, otherwise my tests in cli will pass even though nothing will then work:

cli app stucture

.
├── README.md
├── lib
│   ├── cli.ex
│   └── manager.ex # interface for the manager umbrella app
├── mix.exs
└── test
    ├── cli_test.exs
    ├── support
    └── test_helper.exs

Problems

So, following this pattern, I have a few issues:

  • every time create a new @type in the manager, I have to update all the interfaces that use it, so the projects using manager as a dependency can have access to the new types I created.
  • every time I change manager's public API, I have to update the projects that use it as a dependency
  • I have a manager.ex file in my cli app, that is just an interface for the manager app. This is confusing, as people reading my code will not intuitively understand this is just an interface so i can create a mock that obeys it (mocks as nouns :D)

Possible solutions?

Given these issues I have considered the following option:

  • create a new app, called interfaces, where I define the interfaces of all umbrella apps.

This would have the following benefits:

  • Changes to the Public API of any app would always be reflected there.
  • All dialyzer @types would be there as well
  • Any project using the Interface app as a dependency would have immediate access to the most updated interfaces and type specs.
  • I would get rid of the manager.ex file because I would import it from the interface app

It would however have the following drawbacks:

  • Any umbrella app would be forced to have 2 dependencies: the interface app, and the app they want to use (in this case, cli would have to import both interface and manager)
  • They would have access to all interfaces and type specs, even if not needed
  • I would still have to change things in two places: the interface app, and the app that implements the interface.

So I am not really sure on how to deal with this issue.

How do you guys deal with this?

2 Likes

Doesn’t :cli have {:manager, in_umbrella: true}? Then :cli can use anything (public) in :manager.

yes, it does. But I still have to create a manager.ex file that is an interface so I can use Mox.
And when you say public, you also mean @types ?

I guess we already had this conversation, but I personally would not mock internal dependencies in the first place. But manager.ex is one file in :cli, which belongs to :cli and is built to its needs. What exactly would you be copying from :manager and why?

1 Like

manager.ex is a behaviour, that represents the public API of the manager app.
The manager app is an implementation of the manager.ex behaviour.

I have this behaviour so I can mock its implementation with Mox.

How would you solve this without mocks? Inject the dependencies directly?
You would end up with the same issue in the end, in the sense that a change in manager would always require another in cli.

I have a feeling this discussion comes up often in different ways because I have not yet cemented a solution :thinking: Or perhaps this is something more general and I am not seeing the full picture.

So either :cli depends on :manager and takes what it gets or :manager depends on :cli and implements the interface :cli prescribes. If you do both you’re in trouble. You don’t want circular dependencies.

Also what you’re seeing here is partly the cost of decoupling things, just that you’re not yet seeing any benefits out of that, because you’re only doing it for mocking and not for general decoupled parts in your system.

So, a few questions:

  1. What do you think about the interface app idea? Overkill?
  2. Since you wouldn’t use mocks, would you inject dependencies directly? What would you do exactly?

This discussion is making me realize that perhaps the behaviour should be in the manager app instead of the cli app.

Good discussion!

If there’s no need on the manager side I’d test :cli by letting it call into :manager directly. No need to switch implementations without good reason. I also wouldn’t opt for an “interface” app yet. If you actually decouple those apps at some point (no hardcoded dependencies anymore) a shared interface might make sense.

Those apps are decoupled because my manager can have different types of interfaces. cli is just one of them, I will eventually add a phoenix-live interface that talks to the manager.

This is way decoupling was something I did in this small project.

If I allow cli to call manager directly, then I will be doing end to end tests only, that is not my objective. Or perhaps I miss understood you?

1 Like

Yeah, but :cli doesn’t need to be able to interact with multiple backends I imagine. :manager is the only one it interacts with. So no decouling in that direction.

So what? To me this is not a problem in itself. If there’s something in manager, which makes testing hard, this might change. But for that fact of the part making it hard, not because I want to not have end to end tests.

2 Likes

Don’t know if it helps, but here are my 2 cents how we solve it.

We have the following umbrella structure:

.
├── apps
│   ├── api_client
│   └── app

api_client exposes this function ApiClient.Companies.list
app uses the api_client app as a dependency (in_umbrella: true).
Now let’s say we are writing tests for app that do something with what ApiClient.Companies.list returns. First we create a wrapper for ApiClient inside the app. So we never call ApiClient.Companies.list outside of this wrapper directly. This is also where we introduce a new behaviour and use Mox to create a mock for it.

1 Like

@egze so, it is something like the Facade pattern? Or am I missing something?

I guess. Never thought of it in terms of patterns.

I took this approach from this blog post https://medium.com/onfido-tech/the-not-so-magic-tricks-of-testing-in-elixir-2-2-acdd0368572b
See part about “Don’t mock what you don’t own”. In the context of app, we don’t own api_client, so we make a wrapper for it.

2 Likes

@Fire-Dragon-DoL Have you seen the approach outlined in Application Layering - A Pattern for Extensible Elixir Application Design?

I have used a similar design where a behaviour is defined for each Umbrella app’s public API module and the implementation module is configured via the Mix environment. For :dev and :prod it uses the default implementation, for :test it uses a Mox mock.

This allows the tests within one app to stub responses for calls to any other app. You can also use Mox’s stub_with/2 function to forward calls to the mock on to the actual implementation if necessary. This can be a useful approach where you can mock some calls, but allow other calls to be handled as normal.

@slashdotdash I’m guessing this was meant for the OP?

Although it’s interesting that it comes from you, since I think the entirety of these problems disappear when you use event sourcing :wink:

After reading through this discussion I realized I had a circular dependency issue, which I have now fixed with the knowledge gained from reading this thread.

Thank everyone!

The solution I opted to go with was to use create a behaviour in the manager module that exposes its Public API and then import it in the applications that need it.

This does bring up another question, but other than that, I’m all good :stuck_out_tongue:

Hello, not sure if it helps but have you looked at https://github.com/sasa1977/boundary ?

Personally I’d be really happy if something similar could be introduced in Elixir apps (either from an umbrella app perspective / dependencies perspective).

@WFransen An overall interesting idea that forces you to think about your project in a different way. I believe this is not needed for more experienced developers, but for teams that are new to projects or that change constantly I believe it to be quite a nice tool.

In my specific case, it wouldn’t help much, but I see the value in it.

1 Like