Domain-Driven Design with React: Building Scalable and Maintainable Applications

How companies and dev teams can build scalable React projects

Fernando Doglio
Bits and Pieces
Published in
10 min readMay 25, 2023

--

Have you ever found yourself knee-deep in a messy React codebase, desperately trying to untangle the web of components and services? It’s a frustrating experience that can leave even the most seasoned developer feeling overwhelmed and defeated.

But fear not, my fellow developer! There’s a solution that can help you get organized and take control of your React application: Domain-Driven Design (DDD).

Now, I know what you’re thinking. “Oh great, another fancy buzzword to add to the tech jargon dictionary.” But bear with me, because DDD is not just another buzzword. It’s a design approach that can help you build better React applications, one component at a time.

💡 If you’re not too familiar with the term Domain-driven design (or DDD), give this article a read to get a better understanding.

In this article, I’ll dive into what DDD is and how it can help you improve the architecture of your React application.

I’ll explore how to structure your codebase using bounded contexts and the importance of separation of concerns.

I’ll also provide concrete examples of how to implement DDD principles in your React application, including using hooks and context for state management.

So grab your favorite beverage, put on your thinking cap, and let’s explore how DDD can help you build scalable and maintainable React applications.

Also watch:

Company X

Meet Company X. They have a complex project that involves managing user accounts, tracking inventory, and processing orders.

As the project grew, the engineering team over at Company X started noticing that the codebase was becoming increasingly difficult to manage. Components were tightly coupled, state management was inconsistent, and the code was difficult to extend or modify.

They realized that they needed to restructure the codebase using Domain-Driven Design (DDD) principles. They started by identifying the main business needs of the application and creating bounded contexts for each one.

For example, they created a User Management context that encapsulated all the functionality related to creating, updating, and deleting user accounts. They also created an Inventory Management context that handled everything related to tracking inventory levels and processing orders.

By creating bounded contexts, the engineering team was able to separate the concerns of the application and create more modular and maintainable components.

Each context had its own set of entities, which represented the core objects in that particular context.

For example, the User Management context had a User entity, which encapsulated all the properties and behavior related to user accounts. Similarly, the Inventory Management context had an Order entity, which encapsulated everything related to processing orders.

They also used React hooks and context to manage state in a DDD-oriented way. They created custom hooks for each context, which provided a consistent way of accessing and updating the state related to that context.

For example, the User Management context had a useUser hook that encapsulated all the state related to user accounts, such as the list of users and the currently selected user.

Similarly, the Inventory Management context had a useOrder hook that encapsulated everything related to processing orders, such as the list of orders and the currently selected order.

By using hooks and context in this way, Company X was able to create a more consistent and maintainable state management system that was tightly coupled to the needs of each context.

In the end, the project was a success. By using DDD principles, they were able to create a scalable and maintainable application that was easy to extend and modify. The codebase was easy to navigate, and team members were able to work together more efficiently.

👉 Check out this article to learn about tools you could use to implement DDD principles in your applications.

How did Company X apply DDD to solve his problem?

Let’s dive deeper into how Company X structured its components and services in a DDD-oriented way, using hooks and context for state management.

I’m also going to be using Bit to create these components and physically save them in a way that resembles the separation into bounded context that we’re getting through the use of DDD.

When creating a React application using DDD, it’s important to structure your components in a way that reflects the business needs of your application. This means organizing components into bounded contexts, where each context is responsible for a specific aspect of the application’s functionality.

In practice, when using Bit, we can think of a bounded context as a “scope”, which allows us to group our components logically, which is exactly what we need.

For example, let’s say you’re building an e-commerce application that allows customers to purchase products. You might have a bounded context for managing product listings, another for managing customer accounts, and a third for managing orders.

Within each bounded context, you would create components that are specific to that context.

For example, in the customer accounts context, you might have components for creating and updating accounts, resetting passwords, and managing payment methods.

bit create react customer/crud --scope=customer-account
bit create react customer/pwd-reset --scope=customer-account
bit create react customer/payment-methods --scope=customer-account

Those commands will create a folder structure inside your project that looks like this:

Notice how the scope “customer-support” holds all 3 components, which in turn, thanks to me using Bit, have also their documentation files and test files auto-generated for me.

What else can we do?

In addition to structuring components by bounded context, you’ll also want to ensure that your components are modular and reusable.

This means separating concerns and creating components that can be easily composed together to create more complex functionality.

Read this article if you want to know more about what it means to build a web page with composable components using Bit.

One way to achieve this modularity is by using React hooks and context for state management.

Hooks allow you to encapsulate state and behavior in a reusable function, while context provides a way to share state across multiple components without having to pass props down the component tree.

For example, let’s say you have a bounded context for managing product listings. Within that context, you might have a ProductList component that displays a list of products. To manage the state of the product list, you could create a custom hook called useProductList that encapsulates all the state and behavior related to the list of products.

Let’s create both, the component and the hook using Bit:

bit create react products/list --scope=product-listing-management
bit create react hooks/use-product-list --scope=product-listing-management

Read here if you want more details on how to create custom React components using Bit.

Notice how I’m using dashes to separate the words on the hook’s name, however, look at the resulting code:

The folder structure on the left shows the same logic separation, this time the bounded context was “product-listing-management”.

And on the right you see the casing for the useProductList hook is almost right. We just need to change that upper case “U” at the beginning of the function’s name and then update its code to be something like this:

import { useState, useEffect } from 'react';

export function useProductList() {
const [products, setProducts] = useState([]);

useEffect(() => {
// Fetch the list of products from the server and update the state
fetch('/api/products')
.then(response => response.json())
.then(data => setProducts(data))
.catch(error => console.error(error));
}, []);

function addProduct(product) {
setProducts([...products, product]);
}

function removeProduct(productId) {
setProducts(products.filter(product => product.id !== productId));
}

return {
products,
addProduct,
removeProduct
};
}

In this example, the useProductList hook initializes the products state to an empty array and fetches the list of products from the server using the useEffect hook. It also provides functions for adding and removing products from the list.

You could then use this hook in your ProductList component to display the list of products:

import {useProductList} from '@my-org/product-listing-management.hooks.use-product-list';

function ProductList() {
const { products, addProduct, removeProduct } = useProductList();

return (
<div>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
<button onClick={() => addProduct({ id: 4, name: 'New Product' })}>Add Product</button>
<button onClick={() => removeProduct(3)}>Remove Product</button>
</div>
);
}

export default ProductList;

Notice the import path of the useProductList hook. That’s a benefit of using Bit, it creates a symlink on my local node_modules so I can use my components and hooks properly organized by context boundary and keep the code future-proof (once these components are shared through Bit, the import path will remain the same). Read this documentation to understand how to start using Bit with your React project.

In this example, the ProductList component uses the useProductList hook to manage the state of the product list. It displays the list of products using the map function and provides buttons for adding and removing products from the list.

By using hooks and state in this way, you can create modular and reusable components that are tightly coupled to the business needs of your application. This can make it easier to extend and modify your application over time, as your application grows and evolves.

Did you like what you read? Consider subscribing to my FREE newsletter where I share my 2 decades’ worth of wisdom in the IT industry with everyone. Join “The Rambling of an old developer”!

Why DDD?

Incorporating Domain-Driven Design (DDD) principles into your React application yields numerous advantages. The essence lies in adopting a domain-centric mindset that caters to your specific needs.

Simplifying the narrative, organizing your application into modular components results in a codebase that is easily scalable and maintainable — a combination that developers cherish.

This effect is further amplified when utilizing tools like Bit, which seamlessly facilitates component creation and sharing with other devs.

Face it, DDD introduces an unprecedented level of organization and structure to your React codebase.

With DDD, you’ll set clear boundaries and well-defined domains, allowing you to compartmentalize your code in a way that aligns perfectly with your application’s requirements.

But wait, there’s more!

DDD empowers you to create React applications that are not only functional but also highly maintainable and scalable.

By carefully analyzing and modeling your core domains, you’ll lay a rock-solid foundation that can effortlessly accommodate future changes and expansions. Say goodbye to tangled messes and welcome a world of code that evolves with ease (at least, in theory!).

This one’s a controversial one: DDD fosters collaboration. Yes I said it.

Think about it, with DDD, you’ll have a shared language and a common understanding of the business domains you’re working on. And to achieve it, you’ll have to collaborate with others, there is no way around it!

This paves the way for seamless communication, efficient problem-solving, and increased productivity. In other words, it’s like using a lingua-franca where everyone understands each other.

Your team and other teams that get to interact with you, will be on the same page. Magic!

And let’s not forget about the integration capabilities of DDD with React’s modern toolset. DDD perfectly complements the power of React hooks and context, allowing you to leverage these features to their full potential.

Another benefit of using DDD is improved maintainability. By adopting a domain-centric approach to organizing your code, you’ll establish a highly modular architecture that is both comprehensible and maintainable.

This, in turn, minimizes the accumulation of technical debt and facilitates smoother code refactoring in the long run.

The beauty of this approach lies in the ability to separate concerns and define clear boundaries between domains. This not only enhances code testability but also enables more efficient testing of individual components and services in isolation.

As a result, the risk of encountering regressions and bugs is significantly reduced.

The part about isolated components that are easier to test individually is entirely provided by Bit, which is another reason why this is such as great tool for the DDD+React combo. Read this article to learn how to turn your existing components into valid isolated components that can be shared with others.

Overall, by using DDD principles in a React application, you can create a more scalable, maintainable, and collaborative codebase that is easier to extend and modify over time.

Whether you are working on a small project or a large, complex application, adopting a domain-focused approach can help you create a more effective and sustainable codebase.

In conclusion, building a React application with a Domain-Driven Design approach can be a game-changer for developers.

Not only does it help you create a more organized and modular codebase, but it can also make your team members happier and your code more maintainable. Plus, who doesn’t love a good metaphorical castle with well-defined walls and domains?

So, next time you’re faced with a daunting React project, remember to channel your inner architect and think in terms of domains, aggregates, and entities. And who knows, with a little DDD magic, you might just create a codebase that’s fit for royalty.

Implementing DDD Principles Using Component-Driven Development

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

I write about technology, freelancing and more. Check out my FREE newsletter if you’re into Software Development: https://fernandodoglio.substack.com/