wojtekmach

wojtekmach OP

Hex Core Team

Hey! I vaguely remember that someone (maybe @chrismccord?) mentioned that small Phoenix apps can get away with just a single context which is the app itself. This is exactly how I often build small experiments.

Here’s an example: Commits · wojtekmach/twittr · GitHub. Note, we have: Twittr.list_statuses(), Twittr.create_user() instead of traditional Twitter.Statuses.list_statuses() etc. Minor change, but I find it cleaner and more convenient to have everything in one file early on. When I don’t have a clear idea how to name the context or what put it there, I just put stuff into the “app-context” and treat it as a “staging” area. Over time, stuff emerges and I extract it out.

While it might be counter to spirit of “intentional design” (since there’s just one context there’s not much design there, other than I guess naming functions properly), it helps me from “design paranoia” where I’m blocked on naming things.

It works pretty well for me, I’m curious if this is something worth encouraging people to do more often, or seems counter-productive?

First 10 of 14 Posts Switch mode

hubertlepicki

hubertlepicki

There’s no one size fits all. If this fits you, and your style of coding go and encourage that. But make others aware that it’s not the only true way. Damn, “this is the Rails way” made so much damage to Ruby projects that it’s painful to think of it.

Luckily, both Elixir and Erlang are more flexible. You cn use contexts, sure. I just call them namespaces / modules, and do not follow the convention at all. I’d normally namespace my services, sure, but I would not namespace either controllers (and templates, views) unless they’re nested in router. I would not namespace schemas either. Controllers and schemas map to routes and database tables, and does not make sense in my head to namespace them in contexts having some extra meaning.

I do like another abstraction that comes with OTP, which are the applications. And I tend to use them more heavily than the others I think. What I would normally do when starting a project would be to start with umbrella project and two apps in it. apps/ui and apps/backend. In ui I make another heresy of getting rid of the web subfolder and just putting everything straight to lib. I even skip the lib/ui. Everything in this project is scoped within Ui.* namespace, I don’t need a Ui.Web, nor extra directory. It’s hard to reach to the files with such long paths even with Ctrl+P. That’s okay, it’s my project and I decide.

Then I’d put stuff to app/backend that is generally a place for the business logic. If I feel like separating the some things more, I’d create apps/whatever in addition to backend. Usually I’d extract the schemas and abstract the database read/writes and queries. I’d make apps/storage and expose some interface to backend, which in turns exposes some interface to ui. It’s an uni-directional flow where ui has no idea of internal data structures and implementation of backend, which in turns has no idea about internal data structures (including schemas) of storage. Usually there’ll be a few more apps in apps/ folder. Logs handling, watchdogs, handling integration to external services and other infrastructure bits would find it’s own apps.

And then there’s the kicker in form of testing the layers in separation. Right now, I can test my Ui, stubbing out the backend’s functions as needed with mox, and test my business logic, stubbing out the database read/writes.

So the above is the reason I like applications. I don’t mind if there’ll be dozen of them in apps/.

That’s fine. Sorry for lengthish reply and off topic, this been on my mind for some time and had to share :wink:

peerreynders

peerreynders

I don’t think so. You are simply supposed to use all of the information that you can have before starting to formulate your starting position. That means putting in some genuine effort beforehand into understanding the domain that your application is supposed to be solving some problem in. That being said you likely won’t have all the information that you need up front. Trying to divine all the necessary knowledge up front is what leads to analysis paralysis.

Small experiments by definition have a very limited domain and a very tight focus - so you don’t tend to separate things.

This means you are still in exploratory/discovery mode. There is nothing wrong with that. The problem is when people only explore things in code - possibly bypassing the (cheaper) opportunity to talk to people who are dealing with the problems today as that conversation may shed some light on the inherent structure and forces within the problem domain that may help inform your initial design.

And this is the part where many fail. Project hygiene requires that you don’t let separate concerns/responsibilities “cohabitate” in the same place for the sake of convenience as they will tend to “complect” leading to accidental complexity. Refactoring is essential to manage that complexity. Hence the mantra “refactor early, refactor often”.

The problem is that refactoring is real effort. During that time and effort there is no visible progress “from the outside”. So people put it off - taking on technical debt. The longer the refactoring is put off the greater the refactoring effort will be, especially as new features are built on top of the sub-optimal foundation accruing additional rework - and that is how the big ball of mud starts rolling and growing.

So it comes down to seeking out information that is available to you before you start and make a “plan” (the design). However this isn’t a “create a plan and stick to it” kind of situation but instead the planning process is about exploring (and being familiar with) all the available options and choosing the best path at that time.

When the plan (design) is implemented, typically more information comes to light, some options vanish and new ones pop up and the “optimal path” changes and the plan needs be adjusted accordingly (i.e. Refactoring time).

In my experience one of the hardest things to refactor are database schemas (especially when there is live data out there). So typically I like to keep “schema knowledge” out of the code so that it is easier to refactor in the future.

MikeAnthony

MikeAnthony

“While it might be counter to spirit of “intentional design” (since there’s just one context there’s not much design there, other than I guess naming functions properly), it helps me from “design paranoia” where I’m blocked on naming things.”

One good thng about contexts is that you are not forced into using them one way. I really get annoyed when programming purist try and pretend that their concerns and design patterns apply to every one and every situation. Its hubris. They don’t . You are probably fine. the better approach is to ask you some questions.

Are you developing an app that you know will be in service for years to come?. Then perhaps Perry above has a point. You salary is sure and the company is committed for years to it. They probably want you to take your time amd even obsess over seperation of concerns etc

If however you are a startup working on a proof of concept Perry’s point would not apply. You simply cannot afford to have “no visible progress from the outside”. When you obsess and lose investment or market opportunity then “hygiene” won’t matter a wit - the patient is dead.

I find myself in that category with my present projects . I am either working on my startups or working for companies and business who have tight deadlines that determine their future. I’ll let separate concerns/responsibilities “cohabitate” in the same place for conveience every day and the weekends if it gets my clients app to production where he can maximize an opportunity. design purity (whatever that is) be damned.

If like me you came to Elixir for its high concurrency thats even MORE the case if its a new idea startup. Sure Phoneix willperform great if you break out but the idea might just as well flop and fail to get the traffic you chose Elixir to handle. Keep your eye on the prize. get the app out the door and working because if it does take off there will be money to refactor till your hearts content but no investor or market is going to pour in the money because its design is “pure”.

If that app is secure, works and performs well and satisfies its users then do it with one context two contexts or whatever gets you there. You will be fine. Living projects can reorganize. dead ones can’t.

If its a hobby project then I’d probably explore contexts fully and try and build ones with more than one just for the learning experience

mkaszubowski

mkaszubowski

I also think that this might be a good idea, especially in the early phase of the project, when you are not sure how various parts of your system connect and interact with each other.

From what I’ve seen so far, starting the project with multiple contexts often leads to model-like contexts which all just have the same set of functions (list, get, create, update, delete) and correspond to the underlying schemas and database tables. Having one global context can help you understand what the application does instead of what the data looks like. It’s especially valid in Elixir when you can easily refactor by moving functions between the modules without having to worry about the state and side effects.

dimitarvp

dimitarvp

@MikeAnthony Cannot find anything to disagree with you. Practicism and current circumstances > formulas. Always.

That being said, I discovered that when I pursue even half purity while progressing even a time-constrained project, it often times unlocks my brain in ways I could not predict. If you use the wrong design then you are burdening your mind with the wrong idea and thinking about how to solve problems inside that becomes difficult with time.

But that only applies for mid-to-long term projects, namely from 4 months and on. If you do a lot of short-term contracting then the above definitely applies much less (if at all).

dimitarvp

dimitarvp

Yep, seen this in several languages, Elixir included. Then I am onboarded and I am just “WTF?” and ask the person who authored it what is the purpose this thing serves outside of just having a separate module with DB-specific code. Sadly often times people cannot explain because they simply follow recipes. :frowning:

Not sure if any programmer celebrity ever said this but ever since I work with FP languages, I religiously pursue minimum file count. Context modules, as everything else, must have a very good reason to exist before being created.

jordiee

jordiee

I personally like the idea of only using Phoenix as a communication interface and the actually logic/work is done in a separate application that is called from phoenix. I have an application I am working on currently that is a single phoenix application that has 7 “sub-applications” in its mix.ex file that actually handle all the logic/work.

dimitarvp

dimitarvp

I am fully with @hubertlepicki here and it’s a shame we are not colleagues. We would work perfectly together.

  • Modules (or namespaces of them) capturing certain business purposes work perfectly for the human brain. It’s intuitive and it’s instantly recognizable if one is not hung up on a ton of conventions.
  • Library and framework artifacts make sense for namespacing only if it adds clarity, e.g. Ecto’s schemas should go in YourApp.Schema.User. It’s a trained brain trigger that this is a framework / gluing module. Depending on project complexity, naming like YourApp.Schema.RDBMS.User and YourApp.Schema.KV.Avatar could work as well.
  • App for every collection of modules that is semantically too different from the others. @hubertlepicki gave perfect examples: logging, monitoring, 3rd party APIs, database-specific code, infrastructure.
  • Entirely separately (in an app) should be the business logic. For example, cancelling an order entails changing its status, adding a timestamp when it was cancelled, marking it pending or cancelled in the fulfillment system, etc. So you sould have YourApp.Order.cancel_order/1 function that knows about the steps and calls other modules (most of the time storage-centric ones) but has no idea about the storage itself.

Additionally, minimalistic directory depth. Not sure if umbrellas allow that but I would shorten all paths of the kind apps/storage/lib/storage/module.ex to just apps/storage/lib/storage.ex. I don’t expect to have two apps inside an app that is already a part of an umbrella app so these default paths don’t make much sense to me. There could be a good reason for them though.

Another thing I would do is change lib/yourapp/application.ex to lib/yourapp_application.ex. I feel like the OTP wiring belongs on as a higher level as possible.

And finally, minimum amount of files. Deleting the default lib/yourapp.ex is one of the first things I always do when starting a project. If I really need such a central point in my code then the need will be already known in the future and the file will have good reasons to be recreated.

EDIT: Regarding the minimum amount of files, I also mix my GenServer wiring and the actual functions that do cast / call to it. Every GenServer is, generally speaking, a stateful helper and a gateway to side effects, so I see no reason to split it in two files.

dimitarvp

dimitarvp

That’s also exactly what I do. Be it a website (Phoenix), or a REST API, or GraphQL resovers, they all should just be wiring / boilerplate that connects to the real actions that reside elsewhere in your app forest. And those real actions should be storage-agnostic and endpoint-agnostic. They should not care if ther results are served through a JSON firehose, a total JSON request, XML objects, streamed to a GraphQL client, or whatever else.

AstonJ

AstonJ

I love how Phoenix and Elixir offer you the flexibility to do things how you want. It’s awesome that we have at least three routes emerging as being so widely used that you can feel confident about using them, knowing that there will be others who can help you in making decisions or if you get stuck as the concepts won’t be alien to most people in the community.

I actually think this is one of Elixir’s killer features; there’s not just - like in some other languages or frameworks - a single “____'s way”, there are several! And several that have got the support of the community after having been discussed and identified as a great way to architecture and build certain kinds of apps.

Phoenix is so flexible that you can use it in several ways:

  • As a (albeit more majestic) monolith - so great from those coming from frameworks like Rails or for when you want to make a simple/small site or when you just want to get something up/a MVP, or even as @wojtekmach says in his OP, for use as a starting point.
  • The middle ground and the default Phoenix way - with Contexts. I would probably use this for sites that are not small or simple. This will probably turn out to be the most popular way to build Phoenix apps as most will be a great fit for them.
  • The Replaceable Component Architecture - either via Elixir’s Umbrellas or Dave’s way. I would do this for the largest/most ambitious or long-term apps. This basically takes Phoenix’s Contexts that little bit further.

It doesn’t stop there either - Phoenix is so flexible that you can even have multiple Phoenix layers in an Elixir app - imagine trying to do something like that in Ruby :lol:

Thank you Chris, José, Dave and everyone else who have helped make all this possible - I really do think it’s one of Elixir’s killer features :purple_heart::orange_heart::yellow_heart::hearts::green_heart::blue_heart::purple_heart:

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
mudasobwa
While I am working on the Language Agnostic Code Audit SaaS, which uses MetaAST (spoiler: I am expecting it to be in a good shape for ann...
New

We're in Beta

About us Mission Statement