hckrnws
Reads like a middle part of the post... What exactly does author mean by "onion layering" and what are they being opposed to?
The author supplies some examples (new API field, new microservice dependency, new library dependency) but their fixes seem to apply to any well-organized code - I don't see why you'd need to use some mysterious "onion layering" to create a wrapper around insane dependency and isolate it from the rest of the code.
(Google searches show only a few results and they all seem slightly different, so I am guessing this variant of "onion layering" is something author made up but didn't share with anyone - their blog has only one post)
I think you're looking for https://jeffreypalermo.com/2008/07/the-onion-architecture-pa...
Except your link has layers as "(Infra / UI / Tests) -> application -> domain services -> domain model", with "core" cross-cutting via inner 3 layers
While OP has "config -> (API / Infra) -> core"
Seems pretty different... I don't think author was referring to that particular page.
Yeah, I was confused. This is basically just, "have well defined interfaces, and pass simple data through them", but with no concrete examples.
Thank you for the feedback on the lack of context. Added some preamble and related background articles.
The article is describing, in a less clear way than they could, a clear separation of concerns in code. This is a good practice, especially for complex code.
There is a tradeoff though: adding too many layers of indirection makes changes just as complicated as if you wrote a big ball of spaghetti (oh, just propagate this field across all 10 layers!), and that your team needs to be diligent about making changes to the correct layers.
With a small, high-quality team this absolutely pays dividends in terms of ease of migration and changes, but you need to ensure there is good knowledge of the architecture spread across multiple people.
The ideal place for this is in code with unclear, or rapidly changing business requirements.
Note that this doesn't prevent you from getting bogged down by excessive complexity -- it literally just limits the blast radius of this complexity and makes the inevitable future refactorings smaller in scope.
> The article is describing, in a less clear way than they could, a clear separation of concerns in code.
My take is a bit harsher. The article tries to reinvent the wheel -- poorly -- and fails to do the faintest and most superficial researches on a topic which is already covered in basic courses on software engineering.
Layered architecture is a ubiquitous name in any work on software architecture, such as popular offshoots such as Uncle Bob's Clean Architecture. They describe the benefits of organizing software projects around layers, but go a step beyond and effectively solve the problem by also classify what kind of responsibilities should be assigned to each layer and how the dependency relationships between layers should be managed and enforced. I'm talking about basic principles such as internal/lower-level layers should contain code that changes the least frequently.
There are also some software architectures which abandon the whole concept of layer and instead specify organizing the project in independemt submodules implementing/sharing common interfaces, and have root project handle only integration. This is often dubbed horizontal architecture as in all modules are laid out horizontally, as in they specify no dependency relationships with modules represented above or below, instead of vertically, as in modules are expected to comply with a fixed relationship archetype.
> adding too many layers of indirection makes changes just as complicated as if you wrote a big ball of spaghetti (oh, just propagate this field across all 10 layers!), and that your team needs to be diligent about making changes to the correct layers.
"Layers of abstraction can solve any problem in software design, except for having too many layers of abstraction" :-)
> Imagine the most annoying possible client of our microservice. Suppose they want a new field, and they insist that it be named a certain way.
This shows a fundamental misunderstanding of what code is for, at least in a production setting. Code isn't a marble statue that's supposed to be safely guarded in a museum, it's a way to get things done. Sure, for a hobby project you can polish it and perfect it like a diamond, but for business needs, the end goal of code is to do The Thing. That annoying client is why you're getting a high salary, if they didn't have their annoying needs, you wouldn't have a job.
There is a lot more to meeting a customer’s needs than blindly doing what they say. Sometimes, you need to write code to protect them from their own (sometimes-but-not-always rational) decisions - or to protect other customers and stakeholder from them.
You also need to keep your own team nimble. I’ve seen plenty of systems that have just thrown code at the product, and they end up with SQL tables with 150 columns - most of which nobody uses, and which can’t be removed, and the customer ends up pissed off anyway because you can no longer turn their requests around quickly.
A good, layered design mitigates against this and makes it possible to continue to be productive even in the face of customer demands.
Externalising customisations is a huge benefit to productivity.
The article is extremely short and by an unknown author, so there isn't much to discuss.
But I've met many people who hated onion architecture with a passion.
I have a few theories:
- Maybe a lot of programmers have ADHD, are autistic, or suffer from dyslexia, and find planning, naming, designing abstractions as excruciating activities.
- Onion architecture etc. is a long-term strategy that mainly benefits the company/project owner, but not the individual contributor. So it basically has to be forced upon programmers, who will resist it in every way possible, because they have no real incentive to use it.
- It's supposed to make writing software easier. But it really requires an IDE that's designed for abstractions, such as IntelliJ, and also requires a different way of working with the code. It's also verbose. So it's really a different paradigm, and it won't work if you use a plain text editor. You'll drown in code and a vast number of files.
- Onion architecture is not OOP, but often mixed in with enterprise OOP, and therefore bad associations that come with enterprise OOP.
Any other thoughts on why people resist it so much?
And what changes in how we work with code, would make onion architecture more practical?
>- Maybe a lot of programmers have ADHD, are autistic, or suffer from dyslexia, and find planning, naming, designing abstractions as excruciating activities.
I don't know about the medical conditions, per se, but I think this does bring up a point that is often overlooked when discussing best practices: our brains are different and organize things in different ways.
What works and makes sense to one group of people might not work or make sense to another group of people. I find that more literal-minded people are frustrated by what they see as unnecessary abstraction and are fine with duplicated code whereas people who think in abstractions have no problem seeing the bigger picture and are proponents of abstractions when the abstractions make sense to them.
I have coworkers who will look at a codebase with a layered/onion architecture and immediately understand and reuse all of the abstractions without issue and others who will immediately want to simplify it and change it all into concrete implementations. I find myself to be fairly evenly split so I see it from both sides.
I think it's often more about the nature of the latest person who looks at the codebase than the codebase itself. Eye of the beholder and all that.
>>does bring up a point that is often overlooked when discussing best practices: our brains are different and organize things in different ways.
I really notice this when I first look at a code base...Its not that the code is 'bad' but more of a 'what were they thinking' when the code was laid out. Eventually you get used to it, but its a bit of a shock when you first encounter it as the organization, data structures, etc are so 'alien' to how you would organize it yourself...
> What works and makes sense to one group of people might not work or make sense to another group of people. I find that more literal-minded people are frustrated by what they see as unnecessary abstraction and are fine with duplicated code whereas people who think in abstractions have no problem seeing the bigger picture and are proponents of abstractions when the abstractions make sense to them.
I don't agree. I don't think this is an issue of having different points of view. It is an issue of being oblivious to constraints that result in this specific configuration.
If they do not understand the problem, they don't understand the solution either.
No one adds abstractions and interfaces because they like complexity. In layered architectures, interfaces and abstraction layers are used extensively to manage dependency relationships.
Take for example dependency inversion. Dependency inversion is used extensively to ensure that inner layers, the modules which should change less frequently, do not depend on implementation details implemented by outer layers. Your inner layers need to get data from other services, save data in a data store, etc. But you should not need to change your inner layers just because you need to call a service. Consequently, you eliminate these dependencies on external services by having your inner layers provide the necessary and sufficient interfaces they require to handle external data, and then have the external services implement these interfaces provided by internal services. This can be interpreted as an unnecessary layer of abstraction if you do not have a clue about what's happening. I mean, your inner layers could simply call a database, but instead they have an interface that's implemented God knows where, and then you need to hunt down how it's used. Except this abstraction layer is of critical importance.
Have you found anything that makes everyone happy and productive?
I sometimes wonder if we'll replace traditional design patterns, especially OOP, with new patterns, that are neither OOP or FP, but perhaps a different paradigm (e.g. how Prolog is wildly different from C++).
>Have you found anything that makes everyone happy and productive?
Not really. It's kind of a constant push-pull and a lot of compromises. In the end it's just about getting things done and dealing with the friction in stride when it comes.
I've often wondered what it would be like working at a workplace where everyone programs with the same mental models, but have never worked at such a place myself and I'm not even sure it's possible.
> Onion architecture etc. is a long-term strategy that mainly benefits the company/project owner, but not the individual contributor. So it basically has to be forced upon programmers, who will resist it in every way possible, because they have no real incentive to use it.
Although I was aware of it for a while now, I had never seen this misalignment (between what I called ease-of-writing and ease-of-maintenance) stated so clearly in so few words. Thank you !
This article doesn’t define onion layering, I am none the wiser from reading it. There are near identical sentences within it, it gave me the sense I was reading AI generated content, or at least heavily AI post-processed content.
Onion layering is a non-scalable architecture which was immensely popular in the 00-10's due to how it interacts with OOP and it's somewhat crazy principles. It's based on seperating everything by interfaces in a structure which is a little similar to a MVC architecture. So you're going to organize your project(s) into a structure where you centralize everything with a seperation based on what "something" does. So you're going to put your OOP models in a specific place, your services in another and so on. As you can imagine it scaled horribly and today it's made completely obsolete by domain based architecture which is basically Onion Layering but both more reasonably organized and actually scalable.
A lot of CS academics still live in a world where OOP is the greatest thing ever. Where things like wrapping functions in classes are necessary and where things like the onion architecture is still "modern". So naturally a lot of inexperienced developers, or developers who've never had to work on large projects, still hail it as the holy grail.
Note that this was a very opinionated post.
>>Note that this was a very opinionated post.
Bonus points for calling it out as opinion, instead of treating it as the 'one true way' :-)
P.S. I still like OOP well enough, and I love formal interface support like Go/Rust use. (Just IMHO)
this looks like it's describing the typical java horror where you have to make a generic interface to every single dependency to pretend you might be able to swap them out at some point and then you drown in a sea of interfaces and indirection trying to abstract everything into purity
please just write plain code that's easy to follow, easy to debug and easy to delete!
you don't need the survivability onion
Yes exactly. Anyone who has worked on big java codebases has at least one horror story where there are 10s or 100s of classes each with "use once" interfaces wrapping them and there are bizarre consequences like for example, there is only one possible way to wire the application up but for "flexibility" you have to go through (and occasionally debug) a spring boot nightmare for some reason anyway because somebody read a blog post on dependency injection and thought that was a good idea.
Onion, Hexagonal is pretty popular in the dotnet world and microservices.
It's also overcomplicated for it's use-case and the main problem becomes mapping between layers and etc.
Eg. One of the code-smells would be the Web.Abstractions layer.
Just go with Vertical Slice Architecture. It dumbs everything down and if you need another technology as DB for example, you'll always need a migration of sorts, even if you have all the necessary abstractions.
Note: Currently on a solo 4 month migration project going from Cognitive Search to Elastic, yes, we use "Onion".
OT, but would you be inclined to elaborate on why you decided to migrate away from CS to Elastic? We’re currently considering CS.
Elastic search has snapshots ( iso deligation) and is cheaper/faster. Cognitive has no backups out of the box.
Ps. ES has an SQLish language that makes it easier: https://rockset.com/articles/elasticsearch-sql/
I don't know what an onion is in programming terms.
But each of those problems can be solved without one?
---
1. "Adding a field to our API"
Parsing: You turn external input into an internal representation.
2. "Introducing a new downstream dependency"
Essentially the same as above, but presumably bi-directional: You also turn your internal representation into something that is understood by your dependency.
3. "Introducing a new library dependency"
I don't quite understand this example. Apparently writing a module that uses a third party library is an onion layer?
---
Can someone explain what I'm missing?
This particular article seems to advocate "adapt all the things!!!" using dependency injection to configure your adapters.
I'd not come across onion architecture before but I'm a big fan of hexagonal architecture. The essential difference is that onion architecture wants dependency injection, adapters and a pure core, where pure just means no dependencies of any kind. Hexagonal architecture has the concept of internal and external, and advocates treating everything that is not part of the application as a consumer. In hexagonal architecture, a database may be an internal implementation detail, or it may be something external things expect to use and have a defined schema, etc. In the first case, it doesn't need special abstraction, in the second, it needs to be treated as something to adapt to. Onion architecture would say that it is a dependency and should be externalized from the start.
Separation of concerns, essentially.
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-a...
I think layers are important (All internal data should be mapped onto DTOs to returned to a client IMHO) but it's also important to not have more than 2 layers to get to the core, ideally 1.
I've worked places before where they have you call through a function that calls a function that calls a function (replace "function" with "api endpoint" in some cases) because "Well we know function X works and so just call it", then fast-forward a year or two "Well we know function Y works, which calls X, so it's safest if you call Y, don't call X" and so on. This leads to a complete mess and tons of inefficiencies. Often "X" over/under-fetches what you want so you either have to throw away some of the result or add to it. I've even seen cases of Z->Y->X where X over-fetches, Y only uses a subset, and then Z has to re-fetch the data that Y threw away. All because someone decided an arbitrary layer is the new golden standard that everything should be built on.
Abstracting everything you use has a (high) cost, and doesn't always achieve what you want. This article repeatedly states things "go no further", but realistically, some dependencies will have worse inconveniences than a bad field name. If database you rely on is relational, has token based pagination, and has eventual consistency, chances are that swapping to a consistent timeseries DB with only limit/offset based pagination will impact code in your *.core.* package.
> Imagine the most annoying possible client of our microservice. Suppose they want a new field, and they insist that it be named a certain way.
Then I tell the client that I sent him the API specification 2 months ago. If he isn't happy with it, he can write a wrapper himself.
Of course, I could also change the API. Whether or not I do that depends on 4 factors in that order:
0) Do I have the time and resources to do so?
1) How important is the client?
2) How much does he pay me to do the change?
3) Exactly how annoying is the client?
Isn't this how everyone (competent) designs systems?
No, this is how everyone incompetent designs systems
Layers of generic APIs required to be 1000x more complex than would be required if they were just coupled to the layer above
Changing requirements means tunneling data through many layers
Layers are generic, which means either you tightly couple your APIs for the above-layer's use case, or your API will limit the performance of your system
Everyone who thinks they can design systems does it this way, then they end up managing a system that runs 10x slower than it should + complaining about managers changing requirements 'at the last minute'
I don't follow your reasoning.
The point of abstraction is to limit blast radius of requirement changes.
Someone decides to rename field in API? You don't need to change your database schema and 100500 microservices on top of it. You just change DTO object and keep old name in the other places. May be you'll change old name some day, but you can do it in small steps.
If your layer repeats another layer, why is it a layer in the first place? The point of layer is to introduce abstraction and redirection. There's cost and there's gain.
Every problem can be solved by introducing another layer of indirection. Except the problem of having too many layers of indirection.
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.
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
> The point of abstraction is to limit blast radius of requirement changes.
No, the point of abstraction is to make things easier to handle.
At least that is the original meaning of the term, before the OOP ideology got its hands on it. A biology textbook talks about organs before it talks about tissues before it talks about cells before it talks about enzymes. That is the meaning of abstraction: Simple interface to a complex implementation.
In OOP-World however, "abstraction", for some reason, denotes something MORE COMPLEX than the things that are abstracted. It's a kind of logic-flow-routing-layer between the actually useful components that implement the actual business logic.
And such middleware is perfectly fine ... as long as it is required. Usually it isn't, which is where YAGNI comes from.
Now, pointless abstractions are bad enough. But things get REALLY bad, when we drag things that should sit together in the same component, kicking and screaming, into yet another abstraction, so we can maybe, someday, but really never going to happen, do something like rename or add a field to a component. Because now we don't even have useful components any more, we have abstractions, which make up components, and seeing where a component starts and ends, becomes a non-trivial task.
In theory this all seems amazing, sure. It's flexible, it's OOP, it is correct according to all kinds of books written by very smart people.
In reality however, these abstractions introduce a cost, and I am not even talking about performance here, I am talkig about readability and maintainability. And as it turns out in the majority of usecases, these costs far outweigh any gains from applying this methodology. Again: There is a reason YAGNI became a thing.
As someone who had the dubious pleasure to bring several legacy Java services into the 21st century, usually what following these principles dogmatically results in, is a huge, bloated, unreadable codebase, where business functionality is nearly impossible to locate, and so are types that actually represent business objects. Because things that could be handled in 2 functions and a struct that are tightly coupled (which is okay, because they represent one unit of business logic anyway), are instead spread out between 24 different types in as many files. And not only does this make the code slow and needlessly hard to maintain, it also makes it brittle. Because when I change the wrong Base-Type, the whole oh-so-very-elegant pile of abstractions suddenly comes crashing down like a house of cards.
When "where does X happen" stops being answerable with a simple `grep` over the codebase, things have taken a wrong turn.
> The point of abstraction is to limit blast radius of requirement changes.
The problem is in many/most? systems there's no way it can possibly do this, because the abstraction that looked like a perfect fit for requirements set 1 can't know what the requirements in set 2 look like. So in my experience what ends up happening with the abstraction thing is people put all sorts of abstractions all over the place that seem like a good idea and when requirements set #2, #3, etc come along you end up having to change all the actual code to meet the requirements and all of the abstraction layers which no longer fit.
To choose a couple of many examples from my personal experience:
- One place I worked had a system the author thought was very elegant which used virtual functions to do everything. "When we need to extend it we can just add a new set of classes which implement this interface and it will Just Work". Except when the new requirements came in we now needed to dispatch based on the type of two things, not just one. Although you can do this type of thing in lisp and haskell you can't in C++ which is what we were using. So the whole abstraction ediface cost us extra to build in the first place, performance while in use and extra to tear down and rewrite when the actual requirements changed
- One place I worked allowed people to extend the system by implementing a particular java interface to make plugins. Client went nuts developing 300+ of these. When the requirements changed it was clear we needed to change this interface in a way a straight automated refactor just couldn't achieve. Cue me having to rewrite 300+ plugins from InterfaceWhichIsDefinitelyNeverGoingToChangeA format to InterfaceWhichIsHonestlyISwearThisTimeAbsolutelyNeverGoingToChangeB format. I was really happy with all the time this abstraction was saving me while doing so.
Most of the time abstraction doesn't save you time. It may save you cognitive overload by making certain parts of the system simpler to reason about, and that can be a valid reason to do it, but multiple layers is almost never worth it and the idea that you can somehow see the future and know the right abstraction to prevent future pain is delusional unless the problem space is really really well known and understood, which is almost never the case in my experience.
I've experienced the same. It's difficult for frontend and backend to communicate because there's a "translation layer" in between. Shipping a new feature is 100x harder than it needs to be because everything has to be translated between two different paradigms.
I feel like systems design is a bit like the Anna Karenina quote, every good software is alike, but the bad ones are different in their own way.
The Gary Bernhardt talk "Boundaries" shows an end result that is very close to The Onion Architecture presented here. And Onion is of course very close to the also popular Clean Architecture and Hexagonal Architecture. Which at the end are very close to applications built using the principles that cjohnson318 mentioned: "have well defined interfaces, and pass simple data through them".
This is all very close to some of the principles Bertrand Meyer teaches. For example, having different modules that make decisions and different modules that perform actions. Which is close to Event Sourcing and CQRS. Which once again is close to BASIC having SUBs and FUNs.
Sure, under a microscope you will have different terminologies, and even apply different techniques and patterns, but the principles in the end are very similar. You might not have anti-corruption layers anywhere, as the sibling commenter mentioned, but that's missing the forest for the trees: the end goal and end result are virtually the same, even if the implementation is different.
In the end happy families have different socioeconomic backgrounds, different ethnicities and religions, but they're still alike. It's the bad ones that have lots of special cases and exceptions everywhere in their design or whatever it is.
No, you do not need to design system like that. Just because there is a chance that something might change (domain logic, library/REST API), does not mean you need to create anti corruption layers everywhere. They limit problems during the (possible but not certain) change but they make code less readable, less performant and harder to test.
Always analyze and decide if it is worth it.
Yes and no. Onion is similar to IO-less Rust or "Functional core, imperative shell" in that it goes one step further from inversion of control/monadic effects and removes all control/effects from the inner layers.
You get some benefits like being able to write very straightforward business logic, but in return you:
- have to constantly fight the entropy, because every day you'll have to implement another corner case that is 2 points if you violate the layer isolation and 10 points if you reengineer the layers to preserve it
- have to constantly repeat yourself and create helpers, because your API layer objects, your domain layer objects, your DB layer objects all look very similar to each other.
Sometimes a transaction script (in Fowler's terminology) with basic DI scaffolding is easier both to write and maintain, especially when the domain isn't rocket science.
It was 10-20 years ago. Today nobody competent does it because it doesn't scale. A lot of the good parts of onion layering still exists in more modern architectures, especially in languages which are still very tied to the original OOP academic principles like Java or C# where you're likely to see interfaces for every class implementation, but as time has moved forward it's not really necessary to organize your functions inside classes, even if you're still doing heavy OOP. So today you're more likely to see the good parts of the onion layering build into how you might do domain based architecture. So even if you're doing models, services and so on, you build them related to a specific business domain where they live fully isolated from any other domain. Which goes against things like DRY, but if you've ever worked on something that actually needed to scale, or something which lived for a long time, you'll know that the only real principle that you have to care about is YAGNI and that you should never, ever build abstractions until you actually need them.
Part of the reason onion still exists is because academia is still teaching what they did almost 30 years ago, because a lot of engineers were taught 30 years ago and because a lot of code bases are simply old. The primary issue with onion layering is that it just doesn't scale. Both in terms of actual compute but also in terms of maintenance. That being said, a lot of the ideas and principles in onion layering are excellent and as I mentioned still in use even in more modern architectures. You'll likely even see parts of onion layering in things like micro-services, and I guess you could even argue that some micro-service architectures are a modern form of onion layering.
The more competent a system is designed, however, is often shown in how few abstractions are present. Not because abstractions are an inherent evil, but because any complexity you add is something you'll have to pay for later. Not when you create it, not a year later, but in five years when 20 different people have touched the same lines of code a hundred times you're very likely going to be up to your neck in technical debt. Which is why you really shouldn't try to be clever until you absolutely need to.
> you should never, ever build abstractions until you actually need them.
This should be put on the cover of every programming textbook, in very bold, very red, letters.
Right next to: "Measure before you optimize" and "Code is read 1000x more often than it is written"
You would hope, but some folks don’t have a background in APIs and don’t really know when life could be better.
What do you mean by background in apis? You mean experience working with them?
I mean designing APIs. Working with APIs is a different discipline than designing them and it takes practice to get it right.
Indirection?
Crafted by Rajat
Source Code