kanonk
I’m currently developing a web application and am wondering if I’m heading in the right direction.
I decided to opt for Dave Thomas’ approach, where each application resides in its own directory. And all dependencies between them is linked through path. i.e.:
# mix.exs
defp deps do
[
{:accounts, [path: "../accounts"]},
{:mailer, [path: "../mailer"]},
]
end
And the dependencies are as follows
web
├── mailer (../mailer)
└── accounts (../accounts)
└── db (../db)
First thing I’m unsure of, is the separation between accounts and db. As the project grows and I need a new module, i.e. products. This would be a new dependency of web, and would also be dependent on db.
web
├── mailer (../mailer)
└── accounts (../accounts)
└── db (../db)
└── products (../products)
└── db (../db)
One of the reasons I bought into this method of dependencies is that it strives to make a module reusable. In this case accounts and inventory would not be reusable applications on their own.
- Taking into account the previous statement, is this still a good approach to structure the application, or should it be made simpler?
Another issue I encountered was configuring the application. I want to test each module individually. This leads to many applications needing the same configuration.
I opted for configuring each application within its own folder, and importing it in any of the applications depending on it.
Here is an example of that: accounts loading config from db
# /accounts/config/config.exs
use Mix.Config
import_config "../../db/config/config.exs"
And
# /accounts/config/test.exs
use Mix.Config
import_config "../../db/config/test.exs"
- Would this be considered good practice for configuring a project like this?
Any insights will be appreciated ![]()
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixir-ls
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
jeremyjh
Not everyone agrees, but I think having separate OTP applications applies when your storage or deployment lifecycle differs between the different applications. It does not offer any additional abstraction or information hiding beyond what the module system gives you.
Examples: if your mailer would almost make sense as a third-party or open-source library, or as a component that is developed by a different team in your organization.
Talking about different applications needing access to the same relational database schema I think is misguided; I don’t see a benefit to this.
What we do in our application is having multi-levels of context modules. So Account, Product are contexts, they may have sub-contexts that are more specialized but this is just represented as a normal module tree. Communication across contexts should happen at the root level. If you just look at the modules and functions involved, this is pretty much what you end up with in an umbrella project anyway.
easco
I’ll agree with perhaps a bit stronger statement, having been there, that it’s a Bad Idea™
If you don’t want to take my word on that, watch just about any video on the “microservices” architecture. Parallels can be drawn between separate OTP applications and “microservices”. Proponents that architectural style are almost universal in cautioning would-be-adopters to avoid allowing the microservices to “integrate through the database”. It leads to a type of coupling that defeats the purpose of the strong separation (and replaceability) granted by having separate applications/microservices.
jeremyjh
Actually the way I think of it: when you have one relational schema, you have one application no matter what you call the pieces of it. OTP applications don’t have to have the same tradeoffs as micro-services because they are just functions in modules that call other functions in modules. But for this same reason, I think having “separate” applications is meaningless if that is all you are doing, it just boils down to architecture of your module hierarchy. Now if you want to introduce broad-grained message schemas into the middle of this, then I’d agree with your concerns. You’ll have the drawbacks of both a monolith and a micro-service architecture. I didn’t think that is what the OP is talking about though.
kanonk
I think we can agree to not delve into that(it’s been heavily discussed in this forum already), and rather discuss if my set up a good implementation of this view.
Thanks for your input. This was a red flag in my mind and why I stopped to think. I got the Idea from a blog post, but probably applied it wrong.
I’ll give som context first. The mailer module has depencies to emailing libraries, but also contains templates.
This one I’m pretty happy about being its own application, although I’m not using it in any other modules than web right now, I might want to use it in i.e. a scheduled task that sends regular mails. I think it makes sense to not couple this with either the db or web application
I checkout out what the generator in phoenix makes for context, like you described, and that makes a lot of sense. This is actually what I’m doing, only I made multiple applications instead of contexts, thanks for sending me in the right direction.
kanonk
@easco your concerns are valid, although this is not what I was talking about / intending, like @jeremyjh suggested.
dimitarvp
@kanonk IMO going as far as having the contexts in a separate app is a bit much, as the other two explained. (Although to be fair, you used paths and not sub-apps in umbrella so my point might be ill-aimed. Sorry if that’s the case.)
That being said, I usually do prefer to have my storage code and config in a separate app (
storage) and make higher-level modules and functions dealing with retrieving and modifying state, in yet another app (domain). Then you can have contexts in your Phoenix / Absinthe / something-else apps use thedomainapp which will in turn usestorage.I am also somewhat disagreeing with @jeremyjh here because making separate apps inside an umbrella is my sanity check; I use it to make sure I don’t leak dependencies – but I don’t abuse it; I only use it to separate topical apps, not go the full microservices route (which at this point is well accepted to be a sub-optimal approach and I agree with that). As for this posing potential deployment hurdles then eh, I am not so sure; setting up the initial deployment is usually a pain anyway so once that’s done you rarely have to think about it again unless something fundamental in your project (or cloud provider) changes.
In short, separate apps are a good technique both in terms of semantic boundaries and also as a way to ensure no leaky dependencies. That’s how I practice it and I am pretty happy with the results so far. This is including mid- and long-term maintenance of Elixir projects by me as well.
kanonk
Thanks for chiming in.
After the first discussion I settled for this kind of structure:
web
├── mailer (../mailer)
└── domain (../domain)
Where
domainhas both schemas and contexts based on schemas andwebis just an interfaceI like your approach, I might do this instead!
jeremyjh
Can you expand on this a little bit? Do you just mean that it ensures that there are literally no direct calls to a dependency such as postgrex in your domain app? The dependency is still in the call-stack though, right (not separated by messaging) ?
dimitarvp
Yes but surely every caller must get to the callee eventually? I was not discussing a high-level SOA architecture. My issue isn’t that the call stack contains the dependency. What I am trying to prevent is the Phoenix controllers / Absinthe GraphQL endpoints directly call Ecto (or any data mapper library).
As a project scales up, you cannot guarantee you will always persist or even query your data same way as before. Having your endpoints call the domain function
Card.add_line_item– which in turn calls functions that use Ecto / Redis / your homegrown cache / Mnesia / anything else – is a sane and low-effort solution to reduce friction as the project needs to evolve. Who wants to modify 50 controller functions when the time comes to introduce a caching layer or go the dual-storage route?I mostly have
mix xrefin mind here – when you need to inspect your call graph before refactoring or introducing new features, having a narrower area between each pair of apps helps a lot.jeremyjh
But how does a separate app help ensure this is the case? You can always call transitive dependencies directly.