Huge thanks to Dave Trollope for his ideas that heavily contributed to this post.
Is there a problem?
Whether or not you have a problem is your judgement call. However, frontend frequently ends up having a complexity issue, which leads to increased cost of maintenance, new feature development, and increased error rates. The backend has it all figured out — microservices/SOA is the agreed upon way to manage a sprawling codebase. In frontends, however, a number of reasons lead to an ever-growing codebase that gets out of hand sooner or later. To list the top offenders:
- With microservices/SOA backends there’s a lot of data assembly through multiple backend calls and subsequent data transformations.
- Frontends are expected to provide cutting-edge and rich UIs, which (at least in some cases) require more frontend logic.
- The sheer volume of features, pages, etc makes for large codebases that eventually become hard to manage.
- Frontend is sometimes the catch-all for all “left over” logic.
- Unlike backends, there’s no industry standard go-to solution that you can fall back on. At least not yet.
What to do?
Before you decide what to do you have to figure out priorities. Managing complexity is a complex issue. Duh! There is no perfect solution out there and you will have to make some trade offs. The right kind of trade offs.
As always, you have to start with user experience first. I’m going to assume that you want to have:
- A modern, rich UI that’s consistent across your entire product.
- A performant frontend that a) doesn’t overload the user’s browser with too much processing or data and b) isn’t sluggish due to many backend calls that have to traverse the internet.
- A frontend that is easy to maintain and develop new features in.
A.k.a. “Can’t we just stick it all in the backend?” Can’t we just create highly coarse grained backend APIs that return everything a given UI page or partial needs?
Hold your horses! Now that you have the solution criteria, you need to examine the possibilities and limitations. Let’s look at each of the three criteria from above and what they mean in terms of implementation.
A modern UX means that:
- To achieve the cool user interactions, you have to do some data assembly on the frontend. Whether it’s infinite scroll or partial updates or predictive inputs, you will have to call multiple backend APIs multiple times and it’s impossible to combine them into one coarse grained API. Modern browsers are good at this — they can handle multiple http calls, concurrent or not, quite efficiently. While it’s good to minimize the number of calls, there isn’t anything inherently bad about them.
- Modern browsers can handle larger data sizes and transformations than you probably think. Don’t optimize prematurely.
- Consistency is important. Componentization — need I say more? Components enable UI consistency and ease of maintenance as changing a component will update its look everywhere. All modern JS frameworks offer ways of doing this. Similarly, having a shared style guide is a great way to drive consistency.
For a performant frontend :
- Definitely stick it all in the backend as long as the UX you want isn’t impacted. It can be more efficient to assemble data on the backend and return it in one API call. That’s a good idea (if it doesn’t break your UX).
- Make all those backend calls in parallel as much as possible if you have to make multiple calls.
- Use parametrized APIs on the backend to return only as much data as needed for your frontend to lessen the payload. It is an accepted modern practice to tailor backend APIs to the frontend, going as far as designing different backend APIs for different frontends (e.g., mobile vs. web, etc).
And finally the reason you are here, tada… codebase maintainability:
- Microservices. I mean micro frontends! But don’t try to mimic backend microservices boundaries as those are frequently centered around business domain areas. The frontends, however, are typically split along user flows. For example, search & search results pages together, product details pages, etc. The Conway Law is a good guide to remember too (also see #3 below).
- UI components provide a way to reuse code across your micro frontends.
- You can (and should) still have one domain / URL so to the customer it all looks like one seamless experience.
- Finally, note that this neatly fits into feature team model of org structure and ownership and enables more autonomy for each team in a larger org. This organizational model reduces the communication overhead and the need to synchronize work between teams.
Please, please, please — lets abstain from the holy wars about what exactly is a micro anything and how many lines of code that is. What’s important here is the idea of splitting code bases (typically along some domain boundaries) to decrease code complexity.
trade offs, argh…
So what trade off are we making here?
- Overhead in managing multiple micro frontends. Is this really such a big deal though? This is a known problem and for the most part we can apply the same solutions we already use for backend microservices. Without getting too deep here, the common approach is to structure each service the same way, build, deploy, and run services the same way, use same frameworks and tools, and generally make sure that creating and operating a new service is no different than other services.
- No 100% DRY code reuse. At this point 100% DRY thinking has been invalidated in our industry. It produces too much overhead and is thus not desirable. Looking at the backend microservices as examples, this isn’t any different and we can apply the same solutions. Note, that we still get most of the benefits of code reuse and should have a high degree of DRYness. Typically you create and publish private libraries to share code between services and the rare copy/paste of a few lines is not a problem.
tl;dr version (a.k.a. summary)
- Put as much logic into backend as possible, but not to the detriment of the user experience you want.
- Split frontend into micro frontends. Typically you’d want to do it along team ownership lines, which, in turn, should be defined along business subdomains and user flows. For example, one team may own search & search results functionality, another may own details pages, and so on.
- Use a shared library of UI components, shared private libraries, and style guide to drive consistency and code reuse between micro frontends.