Domain Driven Design in React apps

In articles about DDD, UI is often mentioned as a disposable (Infrastructure) layer. This probably was true for "traditional" applications, where bulk of the logic resided on the servers interacting with users through generated static HTML. In contrast, today, it is not uncommon that main application logic lives in browser, in React and javascript code. Servers (or rather "serverless" cloud) just provide a disposable (interchangeable) persistance infrastructure. So, in many modern applications, UI (Javascript and React components) must contain Core, Domain and API layers (here I quite loosely follow onion architecture terminology). It seems to be instructive to try to map main DDD concepts and architecture layers onto constituent parts of React applications, analyzing possible implications for project structure, development methodology and tooling.

Infrastructure and Core

As already mentioned, in React applications cloud based services may often function as Infrastructure. Common component libraries (Material Design, Ant Design, etc) also mostly belong to this layer. The code responsible for interaction with the cloud is a good candidate for the Core layer. The Core layer may also include some common components, used across whole application and forming distinctive application interface. This is mostly "atomic" components according to Atomic Design terminology. The Core code may live in separate directories or may even belong to sub-packages, orchestrated by bundlers like Rollup. The Core code is a good candidate for traditional Unit Testing. Visual components, tilted toward graphical design, could be developed/maintained using tools like Storybook.

Domain and API

Though the boundaries between Infrastructure and Core layers are more or less clear (though in real life things may be quite fuzzy), the borders between Domain and API layers, and bounded contexts are inherently very subjective and prone to changes.

In case of popular Redux architecture, bounded contexts tend to map to Redux modules. Corresponding Domain logic (and associated Ubiquitous language) could be scattered across actions, reducers, selectors and Redux Sagas. Actions look like a good place to look for thoughtful naming conventions and API. Redux Sagas is a first stop when implementing complex Domain logic.

It is quit natural to organize Bounded contexts in separate folders/packages. In terms of testability, such code is relatively easy to test using common Node.js TDD frameworks like Jest. Functional paradigm, favored by Redux, really shines in this area, especially in frameworks like Redux Sagas/redux-saga-test-plan allowing to test every step of complex asynchronous algorithms.

Quite surprisingly, in this architecture, by a method of exclusion, some compound React components also must belong to API and Domain layers. Which on the second thought is quite natural. For example, a typical application button may have a meaningful, domain specific name and usually is implemented using core application controls:

const PersistOrderButton: React.FC = (other)=> {
    return <PrimaryAppButton onClick={()=>dispatch(persistOrderCommand())} {...other}/>
}

The look of such a button is transparently determined by Core application component (here PrimaryAppButton). In case of Redux, often, there is also one to one correspondence between many redux actions and buttons. This all makes us to think about buttons as belonging to API layer.

The same applies to many other control components, which effectively could be equated to commands. The same logic also applies to components representing Entities. For example, in case of orders, <OrderItem/> components may represent OrderItem entities:

const OrderItem: React.FC = ({id, ...other})=> {
    const {name, qty} = useOrderItem(id)
    return (
        <ListItem {...other}>
            <ListItemField>{name}</ListItemField>
            <ListItemField>{qty}</ListItemField>
        </ListItem>
    )
}

Here again, component view (appearance) is completely determined by generic core components (ListItem and ListItemField) and can chang without any modifications to the component itself. So the OrderItem and similar components could be classified as Domain or API components. The former could be the case when a component contains some non-trivial business logic, the later when it contains very little logic of its own and essentially serves as a frontend to Domain.

Buttons and components representing entities/aggregates, when combined, provide quite a powerful and easy to use API to access and manipulate application entities. And such an API is a natural place for integration, BDD style testing. If someone sticks with BDD development process, one may start from a new test case for a new user story. And initially behavior could be tested in isolation on test pages containing API/Domain components identical to the ones used in an application, though, probably differently layered and spiced with additional/facilitating components. Such test pages can serve as both a project documentation/playground and as a fixture for Cypress tests (yet another popular and convenient tool for tests running directly in the browser).

Not all compound components belong to API and Domain layers though. Pages, layering, template and adapter components form the presentation layer, which may change without any impact on Domain and evidently belongs to Infrastructure. Here we come to the realm of the end to end testing where, again, tools like Cypress come in handy.

Conclusion

From time to time, it could be useful to rethink application structure to accommodate for a new knowledge about business processes and/or to address technical issues and bottlenecks. Domain Driven Design, which recently grew in popularity and adoption across industry, is often viewed as a helpful paradigm, providing new, interesting ways to think about application architecture. Though conceived as a methodology for more traditional client-server applications, DDD looks relevant and thought provoking when applied to purely React applications.