The advent of a sustainable software project
For one of our current Vue apps, we set ourselves a couple of goals that should ideally improve our long-term development performance. This story focuses on the architectural side of things. We’ve got subsequent stories about effective team processes, documentation and others in the making, so stay tuned and follow our Bauer + Kirch publication on medium. But first, let’s take a look at some of the goals that still function as our North Star to this day:
- Mutual understanding: At times, our team had difficulties in finding common ground on how to tackle different challenges in our code. Which is not to say that we were overwhelmed with the tasks at hand. It’s just that different team members would have solved problems in different ways. This lead to prolonged discussions in code reviews, further code changes and so forth. To spare us from all that, we set ourselves out to establish a framework that enforces guidelines for standard tasks without being too restrictive at the same time.
- Maintainability: Some of the code of our legacy projects that run stable in production makes it pretty hard to incorporate changes in a timely manner since everything is so intertwined and obfuscated. For this new app, we wanted to make sure to prevent such cases from the get-go.
- Short ramp-up phase: Similarly, our decisions should allow for new team members as well as colleagues that are not part of the project’s core development team to get started fairly easily. Team composition, as well as internal product ownership, could change in the future and we’d like to be prepared for that.
- Stability: A confusing software architecture makes it easier to introduce unintentional changes in parts of the application not obviously tied to the parts that were changed. Of course, there is a lot more to stability than architecture but it certainly is one piece of the puzzle.
Developing software successfully is more than hacking a number of lines into an IDE. Most of the time, developing software is a team effort — and defining well-considered guidelines helps everyone to stay on course.
The anatomy of Vue.js
Software architecture, as described by Wikipedia,
refers to the fundamental structures of a software system and the discipline of creating such structures and systems. Each structure comprises software elements, relations among them, and properties of both elements and relations.
Let’s try and map that to a typical Vue.js application. To do so, we will identify the concrete software elements as well as the relations among them. Then, we will examine typical Vue.js example projects. Finally, we will try and determine their shortcomings once applications start to scale and move towards a proposition of a sensible architecture.
So, generally speaking, what are the fundamental software elements found in most nontrivial Vue apps? Vue offers a mostly self-contained and incrementally adoptable ecosystem that lets you go from UI-library to full-fledged framework without having to first evaluate a bunch of community options. Conveniently, this puts names to technical domains which might help in making this whole architecture thing more approachable. Note that the libraries themselves are non-integral to the proposed architecture. You may use anything that fulfills the same roles respectively. In our case, we’d probably list the following:
- Components: The main building block of virtually all modern UI libraries.
- State management: In addition to handling data on component-level, the de-facto standard solution for handling centralized state is Vuex.
- Network: It’s likely for a web app to communicate with external services, such as REST or GraphQL APIs. You’d probably do that with e.g. fetch or something more sophisticated like Vue Apollo.
- Routing: Vue Router enables you to navigate between virtual pages inside of your Single Page Application.
- Internationalization: This is handled by the third-party library vue-i18n.
- Styling and animation: Vue promotes the usage of scoped CSS inside of Single File Components. We actually handle styling a bit differently but that’s for another blog post. Nonetheless, we consider styling and animation to be one of the typical fundamental elements.
All of the above are somewhat architecturally positioned in every single web application that makes use of those features — even if nobody put any intentional thought into it. Or as Paul Watzlawick would have put it:
One cannot not have an architecture.
Couldn’t have said it better myself. Okay, enough introductory gibberish.
Let’s talk about folders
A folder structure is not intrinsically coupled to any architecture. However, it is possible to make use of your filesystem to represent parts of it. After all, folders do represent a hierarchical structure; many architectures make use of hierarchies as well.
Vue.js architectures in the wild
Imagine a hypothetical social network app where users can create a profile, upload a profile picture and change some settings around privacy and such. Typical code examples of SPAs that can be found in numerous blog posts etc. often suggest a folder structure that looks something like the one below.
# Folder Structure # Business Domainsrc/
│ ├─── App.vue # General
│ ├─── Avatar.vue # Profile
│ ├─── Home.vue # Homepage
│ ├─── Profile.vue # Profile
│ └─── Settings.vue # Settings
│ ├─── gearIcon.svg # Settings
│ └─── defaultAvatar.png # Profile
│ ├─── router.js # General
│ └─── routes.js # General
│ ├─── profileModule.js # Profile
│ ├─── settingsModule.js # Settings
│ ├─── store.js # General
│ └─── userModule.js # General
└─── api.js # General
As you might have noticed, we labeled each file with its corresponding Business Domain. So far, we discussed Technical Domains exclusively. This becomes even more apparent when comparing the folder structure to the software elements listed further above. As it turns out, the folder’s names directly map to their respective technical roles such as components or store.
Now, we’d argue that at least 9 out of 10 non-developer humans would not end up drawing a picture of much resemblance to that folder structure when asked to design a diagram of said social network. But who said that non-developers are inherently wrong when it comes to technicalities? What probably would end up in such a diagram would be the words Homepage, Profile and Settings which coincidentally make up most of our depicted Business Domain. The only thing missing is General, which marks rather technical files like store.js or router.js anyway.
In fact, we’d go even further and hypothesize that the more technically knowledgable you are, the more likely it is for you to instinctively model architectures around technical abstractions. At the end of the day, it’s all the little quirks of languages and frameworks that occupy your developer-brain most of the time. It seems obvious for your mind to immediately jump to those technicalities.
The Good, the Bad and the Ugly
So, “what’s so bad about a technically-driven architecture” you may ask. As a matter of fact, it is not necessarily bad per se. Especially if your app’s complexity is manageable, going the extra mile and digging deeper into architectural finesse might indeed be overkill. However, as your app’s complexity grows, so does the mental overhead of scrambling together all the scattered pieces of one particular business domain. As you’re assigned a task in the form of a user story, more often than not you will find yourself working in one or only a few business domains. For example:
As a user of the social network, I want to be able to select my very favorite song so that visitors of my profile page can listen to it the moment they enter it.
Sounds familiar enough to those of us accustomed to the agile wonderland that is modern web development. Or MySpace, for that matter. To make that wish come true, you, the developer, have to both make several decisions and implement quite some functionality.
- “Is there some sort of database of songs from which the user may choose from?”
- “Should the user be able to upload audio files?”
- “Where do I put the related business logic? Into a store module? Into components?”
- “Speaking of components: I will need some sort of form for the user to select a song. Didn’t we have a form for that profile pic already? Also, I will probably have to build a small media player to pause and resume the song.”
- And so forth …
None of the above concerns can be addressed with one definitive solution. Oftentimes, there is no clear-cut right or wrong, which is part of the appeal of software development: it’s a creative process! On the flip side, we sometimes long for rules and standards when things get overwhelming at times. If in place though, the feeling of working with your team like a well-oiled machine whose mission is unmistakably clear is one of a kind.
Treating the folders to a makeover
One thing that helped us reach that overall state of happiness was indeed to remodel our architecture with a domain-driven approach in mind. In terms of folder structure, this is what we essentially came up with, again based on the example of a social network:
# Folder Structure # Business Domainsrc/
│ ├─┬─ connector/
│ │ └─── api.js # General
│ ├─┬─ model/
│ │ ├─── store.js # General
│ │ └─── userModule.js # General
│ ├─┬─ router/
│ │ ├─── router.js # General
│ │ └─── routes.js # General
│ └─┬─ ui/
│ └─── App.vue # General
│ └─┬─ ui/
│ └─── Home.vue # Homepage
│ ├─┬─ images/
│ │ └─── defaultAvatar.png # Profile
│ ├─┬─ model/
│ │ └─── profileModule.js # Profile
│ └─┬─ ui/
│ ├─── Avatar.vue # Profile
│ └─── Profile.vue # Profile
│ └─── gearIcon.svg # Settings
│ └─── settingsModule.js # Settings
└─── Settings.vue # Settings
Associated logic which was once kind of all over the place is now neatly structured. Do you notice how each Business Domain is separated, one concern after another? Now, instead of making sense of what to put where, you either expand the appropriate folder and begin your work from there or create a new one.
One downside is the introduction of one more level in the hierarchy of folders. This is why at first glance, this might seem to overcomplicate things more than it helps. Just keep in mind that it’s in the very nature of reduced examples to be, well, reduced. This idea excels as soon as complexity starts creeping in.
Relations among software elements.
Folders sure are fascinating and all, but in the end, they’re just one part of the equation.
This is more of a conceptual high-level overview of our central idea. Here you can see our business domains represented as vertical Functional Slices. Each slice may implement one or more horizontal Technical Layers. Those layers are deliberately named after what they are supposed to represent instead of how that technical domain is implemented. The fact that our Model is implemented as a Vuex store is secondary to the idea of having a Model Layer in the first place.
Establishing a common mental model
Now that we established a common vocabulary, it becomes a lot easier to talk about developing new features as a team. Each technical layer has got a defined responsibility and when thinking “We need to implement a new form in this story”, everybody is pretty much on the same page in terms of implementation details from the get-go. We effectively removed most of the friction that resulted from bikeshedding around technical details that were not part of the actual problem at hand. On a side note, this is essentially the same realization that sparked the idea of developing the code formatter Prettier, just on another level.
- Model: We decided to move practically all application logic into a singleton. There is state involved that doesn’t handle purely visual aspects of our app? Move it into the model layer of the appropriate slice!
- UI: This is the home of our components and styles. Components decide what to render mostly on the basis of what the model is telling them about the current state of affairs. Sometimes though, you might only want to expand a list of items or something. In those cases, it’s perfectly fine to handle that logic locally. It’s unlikely that expanding a list affects the actual state of your application in a meaningful way.
- Connector: As soon as the need arises to talk to external services, we implement a Connector. A Connector implements a set of methods that communicate with e.g. an email service. One could imagine a method
emailService.send(from, to, subject, content)that’s called from the model layer of a Functional Slice
It’s important to note that there are other equally viable schools of thought as well. React even more so than Vue.js embraces the “everything is a component” approach which loosens up the Big Fat Model layer. When looking at React Router, for example, the first thing you are greeted with is this:
Components are the heart of React’s powerful, declarative programming model.
Components are undoubtedly an incredibly useful UI abstraction. They are probably the primary reason why modern UI libraries gained traction in the first place. They can be way more than just the very last layer in a frontend stack that only renders markup. It just turned out that for us, a more classical MVC inspired approach with a strong emphasis on the M worked out better. In an upcoming blog post, we will share insights on how exactly we model our Vuex store in the context of this architecture.
The App Slice
The App Slice takes up a special role. It’s the slice that functions as the main entry point into our application as well as a reservoir for shared components and utils. Speaking in the language of Atomic Design, it’s preferable to have a set of atoms and low-level molecules such as buttons or text inputs to only be present in one mutual slice. That would be the App Slice.
On top of the conceptual structure, we assume a couple of constraints:
- Each Functional Slice may implement any number of Technical Layers. As seen in the example, only App implements Connector. The other slices do not.
- Each Technical Layer may only access the layer directly beneath. It’s for example forbidden to access Connector from UI or the other way around.
- Flow specific slices may only access themselves and App.
- This in turn means that App may only access itself.
The tool dependency-cruiser can be used to “validate and visualize dependencies” on the basis of a set of rules. We did not have the time yet to thoroughly evaluate this tool but the prospect of enforcing our architectural rules with this sort of linter is giving us confidence in the architecture’s longevity. We’ll keep you up to date about any progress made on this front.
In all fairness, we’re not the ones who magically came up with this whole concept. There is a fantastic book on domain-driven design published in 2003 — that is 3 years before even jQuery has seen the light of day — which we cannot recommend enough. We found the idea of bounded contexts especially inspiring.
A word of caution
Dan Abramov, core member of the React team, spent probably at least an order of magnitude more time on all of this than most of us. That should be a good enough reason to listen to his 2 cents on the matter in this thread on twitter:
That’s part of why above, I prefaced the whole section about folders with
A folder structure is not intrinsically coupled to any architecture.
He concludes that
because we’re so bad at programming, it’s important that we often re-evaluate when splitting a problem in a certain way is helpful, and when there’s a way that works better with problems of a certain shape.
And that’s what we’d like to end with. Don’t consider this particular architecture the holy grail of developing Single Page Applications. Do critically think about your problem first. Then, maybe our findings will inspire you to find your own solution. It might turn out that it in fact does fit your use case perfectly. Just be aware that this is not a one-size-fits-all. It worked pretty well for our team and our specific use case.
- Check out Domain-Driven Design by Eric Evans.
- Paul Sweeney makes a compelling case for Micro-Frontends here on Medium. There was a bit of a buzz around them lately and while some people threw some shade and the pattern should certainly be considered carefully, we feel that some of the ideas stem from a similar motivation as the ones illustrated here. It’s not for everyone and it’s a road not much traveled but who knows. It might just fit your use case.
- The Software Architecture Chronicles gives an in-depth overview and is a recommended read for those interested in software architecture in general.
So, did we meet our initial goals? The biggest one for us as a team was to achieve a mutual understanding of what our codebase and the relations within should look like. In that regard, the architecture we came up with is a whopping success. Everyone is on board and feels at home when working on the project. Implementing new features comes naturally and so far just feels right.
In terms of maintainability and short ramp-up phases, it’s too early to tell. We are confident in our decisions but only time will tell if we were right. We’re currently trying to reach stability with the help of tools that keep us in check in regards to our self-imposed architectural rules.
For our reasonably large enterprise Vue.js app, this approach helped us a lot in clarifying responsibilities and enforcing separation of concerns on an architectural level. What are your experiences around architecting SPAs? Be sure to share them in the comments.
In this blog post, we go into more detail on how we evolved the architecture even further in regards to theming, reusability and extensibility.
How to reuse one Vue.js codebase across multiple apps
We develop similar but not identical apps for different costumers. This illustrates how to individualize apps on the…
Also, we will soon shed light on incorporating architecture diagrams in our SCRUM process and how that helped sharpen our mutual understanding even further. There’s a lot to come, so make sure to follow our publication and don’t miss a thing!