Post

My thinking on this one has evolved after working on some medium to large-scale code repos. When I started writing code professionally, I had absorbed some of the best practices common in MVC frameworks, DDD, and other programming patterns. btw, thread length != rage 😜

In my first coding gigs, I experienced first-hand the impact of poorly organized code. SQL queries were scattered throughout template code, everything was mutable, and debugging was painfully tedious. I scoffed and rolled my eyes.

Later, I followed the patterns laid out by the Phoenix code generators. Where contexts contained CRUD code, controllers and LiveViews called those context functions. One of the biggest pain points that has grown out of this pattern, though, is query code - specifically *reading* data from a Repo.

While I can imagine some applications that have identical queries used across multiple call sites, this hasn't been my experience. Instead, I've seen hastily added functions in contexts which serve a single call site elsewhere in the codebase. These functions often have hilariously long names.

This reminds me of the bad old days of CSS before utility classes came along. Each time a new feature is requested, someone has to look for existing code that serves the purpose. This becomes less practical over time, and devs are often nervous about breaking things, so we *just* add to the pile.

Years ago, I heard a hot take on a podcast that stuck with me and I've been taking it more seriously in the past few months. Something along the lines of "just write the query in the controller/LV and move on with your life." IIRC, this was a classic Chris Keathley hot take.

Now, this probably feels like heresy. I didn't start writing queries in my LVs until I was exhausted from the ritual of managing queries in contexts. When I'm in a LV, I know what data I need for its specific use case. I only need to name a local variable and a socket assign. No abstraction needed.

Keeping the create, update, and delete code in a context still makes sense. In fact, I've been venturing out into CQRS land and enjoying the strictness and atomicity of the modules we've created lately. What seems to be contentious is that I'm advocating for single-use Ecto queries inside LVs.

0 reposts0 likes

Haven't thought deeply on this but it pops into my head that the CUD ones tend to make good verbs while a read very often wants name that explains what the data is for. Ridiculously long I believe you said.

Yeah, I tend to think we‘re missing nouns in such cases. Like YearlyReport vs. invoice, user, address, … data. We read many things we never directly write.

This is a loosely held strong opinion, though. I want to hear how this may/will blow up in unexpected ways. Is it *always* a bad idea to write a query in the place where it's needed? #thanksforcomingtomytedtalk 😅

I’m with you. So many things are one-off specifics that using contexts just gets ridiculous and stupid. Then what? Split them up? I’m in favour of writing the specific thing right where you need it and remove the indirection

This is what we do at work and so far it has worked great. We try to keep an eye on common "scopes" (as Ruby on Rails calls them) that show up in multiple queries and extract them out.

For example "upcoming_events" may be such a scope that is extracted. Each LiveView or controller builds its own query based on that scope and then applies the required ordering, filtering, etc.

Hmm…nothing is *always* a bad idea necessarily, context is king. But IMO this comes from a lack of a properly structured application layer. When your app is a bag of functions, it’s extremely difficult to extend and ultimately the safest thing is to append-only. Your individual context functions

I don't think the issue is a "properly structured application layer", as the issue of "appending extra functions to an interface" is the same what happens in other ecosystems, exactly because of the "structure".

Don’t compose (I.e do things that make them not transactionally safe to compose like emit messages etc.) and can’t be optimized (n + 1) without writing a new context function that does both things etc. one solution to that is to just not have an application layer and IMO this is

One reason to keep in context is standardizing business rules. Eg “list users ignores soft-deleted users”, means we don’t need to remember that conditional everywhere and can build on top of that.