DEV Community

Cover image for A different approach to frontend architecture
Will T.
Will T.

Posted on • Edited on

A different approach to frontend architecture

This article aims to introduce a frontend architecture (for applications built with Vue, React, Svelte, etc.) that is easy to reason about and has high maintainability. If you are building a medium/large application and often find yourself wondering about where things should be, this article might be of use to you.

The benefits of a good architecture

Before diving into any technical stuff, let's solve a small problem first:

(image source: https://pusher.com/tutorials/clean-architecture-introduction)

In the image above, can you tell me how to replace the stapler with a tape at a glance? Some of you might come up with an interesting way to do so, but for most of us, we can't immediately figure out how to solve this problem. It looks like a mess to our eyes, and it confuses our brain.

Now look at this:

(image source: https://pusher.com/tutorials/clean-architecture-introduction)

Can you now immediately tell me how to replace the stapler? We simply have to untie the string connected to it and put the tape in its place. You need a near-zero mental effort to do it.

Imagine all the items in the images above are modules or parts in your software. A good architecture should look more like the second arrangement. The benefits of such an architecture are:

  • Reducing your cognitive load/mental effort when working on the project.
  • Making your code more modular, loosely coupled, thus more testable and maintainable.
  • Easing up the process of replacing a particular part in the architecture.

The common frontend architecture

The most basic and common way to separate a frontend application nowadays can be something like this:

The common frontend architecture

There is nothing wrong with the architecture above at first. But then, a common pattern emerges from this kind of architecture where you tightly couple some parts of the architecture together. For example, this is a simple counter application written in Vue 3 with Vuex 4:



<template>
  <p>The count is {{ counterValue }}</p>
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</template>

<script lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
  name: 'Counter',
  setup() {
    const store = useStore();
    const count = computed<number>(() => store.getters.count);

    const increment = () => {
      store.dispatch('increment');
    };

    const decrement = () => {
      store.dispatch('decrement');
    };

    return {
      count,
      increment,
      decrement
    };
  }
}
</script>


Enter fullscreen mode Exit fullscreen mode

You will see that this is a quite common pattern in applications written with Vue 3 and Vuex because it is in Vuex 4's guide. Actually, it is also a common pattern for React with Redux or Svelte with Svelte Stores:

  • Example with React and Redux:


import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';

export const CounterComponent = () => {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  const increment = () => {
    dispatch({ type: 'increment' });
  };

  const decrement = () => {
    dispatch({ type: 'decrement' });
  };

  return (
    <div>
      <p>The count is {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode
  • Example with Svelte and Svelte Stores:


<script>
  import { count } from './stores.js';

  function increment() {
    count.update(n => n + 1);
  }

  function decrement() {
    count.update(n => n - 1);
  }
</script>

<p>The count is {$count}</p>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>


Enter fullscreen mode Exit fullscreen mode

There is nothing inherently wrong with these. In fact, most of the medium to large applications out there are probably written like these. They are the recommended ways in the official guides/tutorials.

However, everything is a trade-off. So, what are the advantages and the disadvantages of this pattern?

The most obvious benefit is probably simplicity.

But for that, what have you sacrificed?

You have tightly coupled the stores to the components. Now what if one day your team finds out that Redux is not the best fit for the application anymore (probably because it is overly complicated) and wants to switch to something else? Not only will you have to rewrite all your stores, you will also need to rewrite the logic of the React components that have been tightly coupled to Redux.

The same problems happen to all the other layers in your application. In the end, you cannot easily replace a part of your application with something else because everything has been tightly coupled to each other. It would just be better to leave it be and rewrite everything from scratch.

But it does not have to be that way. A truly modular architecture can allow you to replace your React + Redux application with React + MobX (or Valtio), or even crazier, React + Vuex or Vue + Redux (for whatever reason) without impacting other parts of your application.

So how do we replace a part of our application without impacting the rest, or in other words, how do we decouple every part of our application from each other?

Introducing a different approach

Introducing a different architecture
The characteristics of the layers are as follows:

  • Presentation: This layer is basically made of UI components. For Vue, they are Vue SFcs. For React, they are React Components. For Svelte, they are Svelte SFCs. And so on. The Presentation Layer is directly coupled to the Application Layer.
  • Application: This layer contains application logic. It knows of the Domain Layer and the Infrastructure Layer. This layer, in this architecture, is implemented via React Hooks in React or Vue "Hooks" in Vue 3.
  • Domain: This layer is for domain/business logic. Only business logic lives in the Domain layer, so there is just pure JavaScript/TypeScript code with no frameworks/libraries whatsoever here.
  • Infrastructure: This layer is responsible for communications with the outside world (sending requests/receiving responses) and storing local data. This is an example of the libraries you would use in a real-world application for this layer:
    • HTTP Requests/Responses: Axios, Fetch API, Apollo Client, etc.
    • Store (State Management): Vuex, Redux, MobX, Valtio, etc.

Applying the architecture

If you apply this architecture to an application, it looks like this:

Applying the architecture to a React app

The following characteristics are referred from the above diagram of the architecture:

  • When you replace the UI library/framework, only the Presentation & Application layers are impacted.
  • In the Infrastructure layer, we have a Facade so that when you replace the implementation details of the store (e.g. replacing Redux with Vuex), only the store itself is impacted. The same goes for replacing Axios with Fetch API or vice versa. The Application layer does not know about the implementation details of the store or the HTTP Client. In other words, we have decoupled React from Redux/Vuex/MobX. The logic of the store is also generic enough that it can be used with not just React but also Vue or Svelte.
  • If the business logic changes, the Domain Layer will have to be modified accordingly, and that will impact the other parts in the architecture.

What is more interesting about this architecture is you can even further modularize it:

Further modularizing the architecture

Caveats

Even though the architecture can decouple the parts of your application from each other, it does come with a cost: increased complexity. Therefore, if you are working on a small application, I would not recommend using this. Don't use a sledgehammer to crack a nut.

For a more complex application, this architecture might probably help you achieve something like this:

The benefits of investing in an architecture

(image source: https://www.simform.com/react-architecture-best-practices)

An example

I have built a simple counter app that demonstrates the merits of this architecture. You can check the source code here: https://github.com/itswillta/flexible-counter-app.

An example

In this application, I have included Vue, React and Vue with Vuex, Redux, MobX, Valtio and even localStorage. They can all be replaced without impacting each other. Follow the simple instructions from the README file and try switching a part of the application with another one.

I know that for this counter app, I'm using a sledgehammer to crack a nut, but building a complex application is a little bit out of the question for me right now.

Questions & discussions are more than welcomed 😊.


If you're interested in Frontend Development and Web Development in general, follow me and check out my articles in the profile below.

Top comments (31)

Collapse
 
jackmellis profile image
Jack

👍 really good article. I've been writing FE apps almost identical to this for a few years now and it's a great pattern to use. Bonus points if you use dependency injection to further decouple your layers!

Collapse
 
itswillt profile image
Will T. • Edited

Hi Jack, thanks for reading the article 😃.

In the example app, I'm already doing some simple manual dependency injections in the Application layer. In the frontend world, we still have to keep in mind code splitting & lazy loading, so dependency injection is a little bit different from what we do in the backend side I think.

Collapse
 
jackmellis profile image
Jack

Yes code splitting is a big issue for DI on the front end. In a node app, for example, you can just grep all of your dependencies when bootstrapping with very little cost. This is actually something I'm trying to work out right now 🤔

Thread Thread
 
itswillt profile image
Will T.

@jackmellis Please write an article about it if you ever figure it out 😍.

Collapse
 
tuanlc profile image
Lê Công Tuấn

Bonus points if you use dependency injection to further decouple your layers!

How do you use dependency injection on the FE side?

Collapse
 
itswillt profile image
Will T. • Edited

@tuanlc I guess you can check out Jack's article about it: dev.to/jackmellis/dependency-injec....

Collapse
 
michaelbailly profile image
MichaelBailly

Hello Huy, thank you for this awesome article ! It reminds me a lot Domain Driven Design, applied to frontend. Well done !

Collapse
 
itswillt profile image
Will T.

Thanks for dropping by. This architecture is indeed inspired by the CLEAN architecture, the Onion architecture and DDD, with a few tweaks to make it applicable to the frontend world.

Collapse
 
talyssonoc profile image
Talysson de Oliveira

What do you think of adding this to the post? Might be useful for people that are new to these concepts to understand the ideas didn't come outta nowhere. We lose a lot to context when we don't reference the canonical source of this kind of info.

Collapse
 
shaalanmarwan profile image
Shaalan Marwan

Hello Sir, is there any resource about how to apply this for react

Collapse
 
itswillt profile image
Will T.

@shaalanmarwan Perhaps in the future I'll write a detailed article about applying this architecture in a React application. But for now you can use the example repo where a React application is included.

Collapse
 
leob profile image
leob • Edited

Great article!

This is known as Hexagonal Architecture, right? DDD (Domain Driven Design) is another closely related concept. Problem is that most devs aren't familiar with it, and that for most small to medium apps it isn't really worth it. But for larger apps I'd say yes, it's worth looking at.

P.S. what I like about this approach is that you raise the abstraction level, and that you focus on the problem rather than the implementation, and you cut yourself loose from endless discussions about Redux, versus Context, versus whatever. What I like less about it is the added boilerplate, if you can get rid of that then you have a win win situation.

Collapse
 
itswillt profile image
Will T. • Edited

I don't know what this architecture should be called. Some say it's like Hexagonal Architecture, some say it's related to DDD. All I know is it got inspired by a lot of architectures 😁.

And yes, the trade-off of this architecture is it will add more boilerplates. So in the end it might not be worth it if you want to quickly build a small application.

Collapse
 
cfuehrmann profile image
Carsten Führmann

Very nice article! I'm a full stack developer used to Clean Architecture on the backend. Obviously, the frontend world has a lot of catching up to do in the architecture department. One specific thing that strikes me how is frontend state management (like Redux) should be passed into presentational components by dependency injection instead of the way it's currently done. It has to be said though that DI seems harder to do nicely in the JavaScript/TypeScript world. One reason why DI nicer in the backend is that backend languages allow DI containers to use injectees' static types (since those types are around even at runtime and can be accessed via reflection, unlike in TypeScript). So it seems a DI container in JavaScript or TypeScript will necessarily have a less convenient API,

Collapse
 
itswillt profile image
Will T.

One specific thing that strikes me how is frontend state management (like Redux) should be passed into presentational components by dependency injection instead of the way it's currently done.

I totally agree. I don't really like the idea of a state management tool being tightly coupled to UI components, but that is probably what library authors want though.

Collapse
 
joruch profile image
Joris

There are some good ways to do DI in the frontend nowadays, with BottleJS for example. Angular also heavily uses DI. Another reason why I don't get why people don't seem to talk about that more.

Collapse
 
cfuehrmann profile image
Carsten Führmann

I just glanced over the docu of BottleJS. It's interesting to see how the DI container API compensates for the language's lack of static types: When you register a service whose "constructor" has arguments, you have to list, during registration, the names of the registrations that should be passed for those arguments. For example, assume that Beer has three arguments:

var Beer = function(barley, hops, water) { /* A beer service, :yum: */ };
Enter fullscreen mode Exit fullscreen mode

Then, when we register Beer, we have to pass the names of the registrations for 'Barley', 'Hops', and 'Water' during registration:

bottle.service('Beer', Beer, 'Barley', 'Hops', 'Water');
Enter fullscreen mode Exit fullscreen mode

In a statically typed language, we wouldn't have to list 'Barley', 'Hops', and 'Water' during registration since they would be discovered via reflection from the typed arguments of the Beer's constructor. This is what I meant when I wrote DI containers are less convenient in languages without runtime types.

Be that as it may, BottleJS looks nice.

Collapse
 
ozzythegiant profile image
Oziel Perez

As far as I can understand this article, this seems to be the way Angular puts together it's apps. Everything seems to guide you to modularize not just features, but components themselves. I've always loved this about Angular, but unfortunately, its steep learning curve and verbosity makes it difficult to work with. If React had done this from the beginning and not encourage sloppy components with no direction as to how to organize apps, I would have chosen React a long time ago for my apps. Instead, I use Vue since it resembles Angular in terms on syntax but it's simplified; now I can just apply this architecture for large apps.

Collapse
 
kirkbushell profile image
Kirk Bushell

Good to read better architectural articles regarding frontend.

However, I would argue that your counter repository is part of the domain, not infrastructure. The code necessary to build out your counter, includes your counter repository. The infrastructure part consists of things like base repositories and their methods.etc.

The code you put in a repository is always an aspect of business knowledge and therefore, belongs in the domain.

Collapse
 
remshams profile image
Mathias Remshardt • Edited

Thank you for the concise article/summary.

We almost do/did everything like that but the facade for the store.
I always thought exchanging the store would never be required/an option. Turned out in one of our projects we had to do exactly that (replacing redux with plain angular services).
Even though we had no facades for the store in place, the separation of the domain and presenters into dedicated services/components already helps a lot. A huge part of the (old) redux based implementations is now replaced and it is/was possible without too much effort/pain.
So even if it seems like a lot of effort in the beginning these abstractions help a lot in the long run.

Collapse
 
itswillt profile image
Will T.

Yes, the benefits of this architecture will really shine the most in the long run even though you might find it a little bit verbose at first.

Collapse
 
tuanlc profile image
Lê Công Tuấn

Nice article!

Collapse
 
danielgomezrico profile image
Daniel Gomez

I understand your feeling, coming from android / iOS, where many projects follow this kind of architecture decisions vs going into frontend projects where it's difficult to find (or at least in my experience) projects that are not coupled with frameworks.

Thanks for sharing

Collapse
 
fabiocosta89 profile image
Fabio Costa

I like this architecture. It is simple and well divided.

Thanks for sharing.

Collapse
 
itswillt profile image
Will T.

Glad to hear you like it!