EXPEDIA GROUP TECHNOLOGY — SOFTWARE

Applying the Single Responsibility Principle to a FE/BFF Layered Architecture — Frontend Architecture Details

A frontend architecture demonstrating the Single Responsibility Principle in action — Part 2/3

Rafael Torres
Expedia Group Technology

--

Photo of modern building façade.
Image by Robert Katzki on Unsplash

This post dives into the frontend architecture details of the approach explained in Applying the Single Responsibility Principle to a FE/BFF Layered Architecture. For extra context, please read that post first.

The Use Case

For this discussion, we focus on a single use case for our hypothetical app:

As an admin user, I would like to see a list of configured hours of operation for my stores, so that I can better manage my staff in different time zones.

To support this use case, we’ll need a table view showing business-hour records with the attributes/columns shown below. To keep it simple for now, we won’t worry about sorting, filtering or similar features.

A wireframe containing a table view of hypothetical Hours of Operation records in the system, with the following columns: Store, Store Country, Store City, Hours, Time Zone, Last Updated.
Hours of Operation Wireframe

The View Model

When designing frontend architectures, it helps to think about a “view model” (at least when it comes to web or mobile apps) as a message contract with the backend services (the BFF service in our case). This is especially true if the Single Responsibility Principle (SRP) is part of the guiding principles. Often, we think about entity models at the UI level, but that tends to conflate responsibilities, so it’s better to stick to a view model. Hence, the first step is to define a view model based on the UI needs.

A diagram depicting a view model object derived from the table in the use-case wireframe. This view model is an Array of items containing each row record with the following attributes: uri, name, country, city, hours, timeZone, lastUpdatedOn, lastUpdatedBy.
The View Model for our use-case. For simplicity, only one row is depicted in the view model object.

State Management

The next concern is handling API calls and global state (we’ll rely on React to implement our views). We already have a way to handle local state in React via component state, but having a robust solution for global state becomes almost imperative for scaling React apps. Because of all the advantages that it provides, we’ll go with Redux by way of redux-toolkit. This will also allow us to handle API (and any async) calls in this layer, using Redux Thunk.

Empowered by the view model previously designed, we can store the API GET (successful) response in a Redux slice that represents our page. We can then subscribe a Redux selector in our React component that reacts to state changes in this slice to update the view.

A diagram of our Redux pipeline workflow. The starting point is our React component. Its UI events trigger Redux actions that are intercepted by the Redux middleware, where async (API) calls can be made to our BFF. Upon the request response, a second Redux action is triggered and then processed by our reducer. The Redux store is consequentially updated with our view model. A selector allows our component to “listen” for relevant state changes and “react” to them.
The Redux pipeline

Rendering

The final concern in the frontend is obviously rendering the UI. As mentioned earlier, we’ll use React to do that job. Once we have the view model worked out, and the corresponding state management logic, we implement our React components. For the corresponding global state changes, we'll just need selectors that select the data that our component(s) care about. By hooking up these selectors in our components, our UI can react to these state changes. Any other local state changes will be kept in React local state.

As illustrated in the above diagram, our React component(s) will communicate with Redux in 2 ways:

  1. Via actions to send events to the Redux pipeline.
  2. Via selectors to react to changes in global state.

The below diagram shows these concepts in isolation.

A section of the previous diagram focusing on the React component lines of communication with Redux. Outgoing events are sent via Redux actions, whereas incoming state changes come in via Redux selectors.
How our React component(s) communicate with Redux

Wrapping up

At this point, we have the basis to start implementing a maintainable frontend codebase. To summarize of our frontend architecture, we have 2 distinct responsibilities:

  • UI view
  • UI state

Keep these concerns/responsibilities in mind as your codebase evolves, and respect them throughout your code. This should help you keep it maintainable as it grows.

Come back tomorrow for Part 3 of this three-part series, where we’ll look in more detail at the backend details of using this approach.

Diagrams are courtesy of author.

--

--