Home / Blog / Domain-Driven Design in Vue.js
Domain-Driven Design in Vue.js

Domain-Driven Design in Vue.js

Filip Rakowski
Filip Rakowski
Updated: May 15th 2022
This entry is part 2 of 3 in the series Modular Vue.js Applications

In the previous article I introduced the concept of Domain-Driven-Design and showed how it can be utilized within Nuxt with their modules. In this part I want to share with you how we applied the same concept in Vue Storefront 1 which is a regular, Vue 2 application.

What do we want to build?

In short words - we want to build something similar to Nuxt modules in a regular Vue.js application. Of course, we will cut down a lot of functionalities and focus only on the ones that we need for this particular use case.

Such module should meet the following requirements:

  • Communicate with the rest of the application through a single entry point just like a Nuxt module
  • Connect with the vital parts of your application like router, vuex, and the app instance so we don’t need to write our code outside of the module
  • Don’t rely on imports from other modules (all communication should be possible through the entry point) to avoid tight relations between modules

This is more or less how we used the Nuxt module in the previous article. Now let's see how we can create this architecture, starting with the module API itself.

What should the module look like?

In Vue Storefront we decided to define each module as a function. The module function has access to all vital parts of the application through a context object:

function categoryModule({ app, router, store }) {
// we can also use app.$router, app.$store but this approah is more readable to me
}

TIP: Putting all function parameters in a single object makes it easier to maintain. It’s easier to remove or add new properties into such functions in the future because their order doesn’t matter.

Let’s quickly review all the parameters and for what we would need them:

The app is our root Vue instance. We will need it for:

  • installing Vue plugins

    Vue.use(plugin)

    CAUTION: For the plugins that are adding properties to the Vue.prototype , it’s important that we register the module before the Vue instance initialization!

  • registering global components/directives

Vue.component('ComponentName', ComponentInstance)
Vue.directive('directiveName', directiveInstance)

CAUTION! Avoid directly adding properties to global configuration like this:

Vue.myGlobalProperty

You can accidentally create dependencies between modules (which is something we try to avoid) if you use these properties in other modules.

The router instance will be used for

  • adding routes
router.addRoutes(routes)
  • registering navigation guards
router.beforeEach()
router.afterEach()

We will use store to register Vuex modules

store.addModule('ModuleName', moduleInstance)

That’s all we need for our module system to integrate with the app.!

Now let’s take a look at the Nuxt module from the previous article:

// index.js
module.exports = function ProductCatalogModule (moduleOptions) {
  this.extendRoutes((routes) => {
    routes.unshift({
      name: 'product',
      path: '/p/:slug/',
      component: path.resolve(themeDir, 'pages/Product.vue')
    });
  );
  // we can't register stores through Nuxt modules so we have to make a plugin
  this.addPlugin(path.resolve(__dirname, 'plugins/add-stores.js'))
}

// plugins/add-stores.js
import CategoryStore from '../stores/category.js'
import ProductStore from '../stores/product.js'

export default async ({ store }) => {
  store.registerModule(CategoryStore)
  store.registerModule(ProductStore)
};

This simple Nuxt module adds a single route and then registers two Vuex modules. It looks like we have everything needed to recreate it with our new API. Let’s try!

import ProductPage from './pages/Product.vue'
import CategoryStore from '../stores/category.js'
import ProductStore from '../stores/product.js'

export function categoryModule({ app, router, store }) {
  router.addRoutes([{
    path: '/p/:slug/',
    component: ProductPage 
  }]);  
  store.registerModule('category', CategoryStore)
  store.registerModule('product', ProductStore)
}

And thats it, even less code than with Nuxt!

Under the hood

Now that we know how our API will look like let’s start implementing the underlying business logic. For now the module is just a function and we have to create the bindings between this function and the rest of the app.

Let’s see how easy it is to create a registerModule function!

// modules.js
// router instance
import router from './router.js'
// store instance 
import store from './store.js'
// vue instance
import Vue from 'vue'

export function registerModule(module) {
  module({ app: Vue, store, router })
}

We’re just passing the app, store and router object into the module function.

Now when we want to register our catalog module we’re doing it like this in the entry point of our application (or somewhere else if the module is not including routes):

// main.js
import { CatalogModule } from './modules/catalog'
import { registerModule } from './modules.js'
import Vue from 'vue'

// instantiate router, store and plugins

const app = new Vue({ 
// options 
))

registerModule(CatalogModule)

And that’s all! now we have a working simple module system in the regular Vue application! This concept is really powerful and can be easily extended if needed.

Communicating between modules

Once your app will become a little bit more complex you’ll quickly realize that some level of communication between modules is inevitable. In Vue Storefront we wanted modules to be notified about certain events in the app like user log in/log out actions.

There are many ways and approaches to this topic and perhaps I’ll write a separate article about this in the future but today I will share the simplest solution to that problem - Event Bus.

We will declare a set of global events that could happen in our app that other modules can listen to and emit. By not putting these events declaration inside any module we’re making sure that they are always accessible and not tied to the implementation details of our auth module.

Let’s start with the Event Bus implementation. If that's the first time you hear this term don’t get scared. In Vue world Event Bus is just a Vue instance that is used to emit and listen to events.

// eventBus.js
import Vue from 'vue'

const eventBus = new Vue()
export default eventBus 

That’s the whole declaration! Now we can use this Event Bus anywhere in our app:

// emit an event
bus.$emit('init')
// we can perform some actions after the event is being emitted by listening to it
bus.$on('init', () => console.log('Hello world!')

The problem I have with Vue events system is that its using strings as identifiers. It’s super hard to debug them if you accidentally make a typo somewhere.

Since we already know what events we would like to be globally available in our app we can explicitly declare them:

// eventBus.js
import Vue from 'vue'

const eventBus = new Vue()

const EVENT_TYPES = { logIn: 'logIn', logOut: 'logOut' }

export default {
  $emit: {
    logIn: (payload) => eventBus.$emit(EVENT_TYPES.logIn, payload)
    logOut: (payload) => eventBus.$emit(EVENT_TYPES.logOut, payload)
  },
  $on: {
    logIn: (callback) => eventBus.$on(EVENT_TYPES.logIn, callback)
    logOut: (callback) => eventBus.$on(EVENT_TYPES.logOut, callback)
  }
}

We used EVENT_TYPES enum to map string identifiers into a data objects similarly like we do with mutation types in Vuex.

With this approach we’re not only eliminating the risk of making a typo but also improving Developer Experience with autosuggestions.

import eventBus from './eventBus.js'

eventBus.$emit.logIn()

The only thing that's left is injecting the event bus instance into our module system so it can be accessed in every module:

// modules.js

// router instance
import router from './router.js'
// store instance 
import store from './store.js'
// vue instance
import Vue from 'vue'
// event bus instance
import eventBus from './eventBus.js'

export function registerModule(module) {
  module({ app: Vue, store, router, eventBus })
}

As you can see extending our module system with additional properties is a straightforward process because we used a single object as a parameter we can easily add new properties without introducing breaking changes to already existing modules.

Now, let’s see this Event Bus in action to understand it a little bit better.

In our app we will probably have some kind of authorization module with a `logIOut action in its Vuex store:

export function authModule({ app, router, store, eventBus }) {

  const authStore = {
    // state, mutations, getters...
    actions: {
     // other actions
     logOut({ commit }) {
       // clear token, end session
       eventBus.$emit.logOut()
     }
    }
  }
  store.registerModule('auth', authStore)
}

After all the steps required for the log out are performed we are notifying the rest of our app about the event so it can react to it.

Now the cart module could listen to this event and clear the cart once the user is logged out

export function cartModule({ app, router, store, eventBus }) {
  // register vuex store, routes etc
  eventBus.$on.logIn(store.dispatch('cart/load'))
  eventBus.$on.logOut(store.dispatch('cart/clear'))
}

This way we’re avoiding tight coupling of modules while still allowing them to communicate with each other. If you follow this approach you’ll cut down the complexity of your app by boiling it down into a set of smaller and scoped “sub-applications”.

Summary

Vue is a very flexible framework. Even though it’s not promoting a DDD concepts with the common best practices it’s straightforward to adjust our application to follow this approach.

Let me know in the comments if you like this series and if you’d like me to explore this concept more in the next articles!

Start learning Vue.js for free

Filip Rakowski
Filip Rakowski
Co-founder & CTO of Vue Storefront - Open Source eCommerce Platform for developers. I'm passionate about sharing my knowledge and thoughts on software architecture, web performance, Vue and raising awaraness about business impact of the technology and code we write. When I'm not working I'm probably playing with one of my cats.

Comments

Latest Vue School Articles

Developing a Full Stack Nuxt App with Bolt.new – An AI Experiment

Developing a Full Stack Nuxt App with Bolt.new – An AI Experiment

Use bolt.new to quickly iterate on a new Nuxt project. Learn how in this article, where we bootstrap a custom blog in no time!
Daniel Kelly
Daniel Kelly
Upgrading Eslint from v8 to v9 in Vue.js

Upgrading Eslint from v8 to v9 in Vue.js

Learn how to upgrade ESLint to v9 in Vue.js, taking advantage of the new flat configuration system and other significant changes in ESLint 9.0.
Mostafa Said
Mostafa Said

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.