Hacker News new | ask | show | jobs
by tomck 703 days ago
Every layer you create is another public API that someone else can use in some other code. Each time your public API is used in a different place, it gathers different invariants - 'this function should be fast', 'this function should never error', 'this function shouldn't contact the database', etc. More invariants = more stuff broken when you change the layer.

So let's say you have some 'User' ORM entity for a food app. Each user has a favourite food and food preferences. You have a function `List<User> getListOfUsersWithFoodPreferences(FoodPreference preference)` which queries another service for users with a given food preference.

The `User` entity has a `String getName()` and `String getFavouriteFood()` methods, cool

Some other team builds some UI on top of that, which takes a list of users and displays their names and their favourite food.

Another team in your org uses the same API call to get a list of users with the same food prefs as you, so they loop over all your food prefs + call the function multiple times.

Amazing, we've layered the system and reused it twice!

Now, the database needs to change, because users can have multiple favourite foods, so the database gets restructured and favourite foods are now more expensive to query - they're not just in the same table row anymore.

As a result, `getListOfUsersWithFoodPreferences` runs a bit slower, because the favourite food query is more expensive.

This is fine for the UI, but the other team using this function to loop over all your food prefs now have their system running 4x slower! They didn't even need the user's favourite food!

If we're lucky that team gets time to investigate the performance regression, and we end up with another function `getListOfUsersWithFoodPreferencesWithoutFavouriteFoods`. Nice.

The onion layer limited the 'blast radius' of the DB change, but only in the API - the performance of the layer changed, and that broke another team.

1 comments

This is where command/query separation is strongest regardless of onion/layered architecture. Your queries/reads are treated entirely separately from your commands/writes so you're free to include/exclude any of the joined data a particular query doesn't need.
I have no idea what you're talking about, my example doesn't include any writes, only a read
Forgive me for not tying it back to your example explicitly.

Your example was a read. So in that case since there's no change in state (no need for protection of the data/invariants) there's no dangers in having different clients read the User records from the datastore however makes sense for them. They could use the ORM or hit the DB directly or anything, really. So getListOfUsersWithFoodPreferences and getListOfUsersWithFoodPreferencesWithoutFavouriteFoods living together as client-specific methods is absolutely fine. It's only when state changes that you need to bring in the User Entity that has all of the domain rules and restrictions.

The idea is that while on Commands (writes) you need your User entity, but on Queries (reads) there's no need to treat the User data as a one-size-fits-all User object that must be hydrated in the same way by all clients.

> So getListOfUsersWithFoodPreferences and getListOfUsersWithFoodPreferencesWithoutFavouriteFoods living together as client-specific methods is absolutely fine

Sorry; my point was that adding this function as a public API 'onion layer' in your code means you're less able to adapt to change. The fact this function returns a `User` entity isn't particularly important - it's the fact when you make a function public, other teams will reuse your function and add invariants you didn't realise existed, so that changing your function in the future will break other teams' code.

Less public 'onion layers' means less of this