Introduction

CQRS pattern stands for Command Query Responsibility Segregation. The idea was derived and evolved from CQS pattern, previously introduced by Bertrand Meyer in his book Object-Oriented Software Construction. The both patterns agree on the formal definitions and separation of Commands and Queries:

  • commands represent actions that alter the current state of the system,
  • queries are ways to read the current state of the system.

In those terms, CQRS, as an architectural pattern, separates write models from read models. It is also complementary with HTTP protocol where altering actions POST, PUT, PATCH, DELETE are reserved for write or command side and GET for read or query side.

CQRS

Reason

To better understand the benefits of using CQRS, we have to understand the problem that CQRS is solving. The problem lies in combination of two factors: collaborative work and data staleness. For example, imagine we have a system with a patient record that can simultaneously be altered by many actors (i.e. users or other software). Lets presume a nurse and a doctor are accessing a patient’s record, and at the same time altering a patient’s data. In such unhinged event, one actor might not be aware of the changes of the other actor since it’s data is stale representation at that particular time, which can result in error of judgment.

Not all the systems require immediate or real time change to be distributed to all clients. Sometimes, it is sufficient that the data renewal can be eventual. With frontend patterns will demonstrate different cases for handling required levels of data staleness.

Benefits

Systems can rely on layered architecture where each request is also responsible for returning the result (i.e. sequential behavior). For example, think of HTTP POST request as a create command and HTTP POST response that reflects a new state - result of its mutation. In such case, the data might be already altered by the time of the receiving the result. Due to stale result, we now need an additional way to perform refetching (referred as query), otherwise we would be limited to consider results as the only source of truth.

In the case of CQRS the command doesn’t need to return any result. If the HTTP POST responses with status 200, client can deliberately consider the request’s payload data as a source of truth and spare a round trip to the server. In most use cases, commands that create records can usually return record ID or multiple records IDs (in batch use cases). This is not a hard rule, exception can be made whenever needed. For example, in case of pop() stack command - result is mandatory and has to be returned. Commands can also side-effect an event to notify the interested actors that the data they have are staled and modified. In some cases, we can also present the current list of commands that are in progress or completed.

We can conclude that command’s purpose is only to track the state mutation success, while query purpose is to deliver state as a source of truth for the system. With that segregation approach, CQRS provides more flexibility when it comes to designing data exchange interface between clients and servers.

Lets extend the previous example in one more detail, lets say a doctor, besides entering patient’s prescription, also adds a contact for a family member. As she tries to submit the form, the request is denied due to prescription detection against patient’s known allergies (e.g. past entries by a nurse). Due to the error, none of the data are saved including the contact data of a family member. With the case of CQRS, we could benefit by splitting it into two commands - one for entering family member contacts and the other for entering patients prescription that requires allergies detection.

It’s important distinction that none of commands need to carry the burden of forming comprehensive query results since each command success is sufficient for UI to indicate the success on changes and that (optionally) a query invoke is necessary for fresh fetch. Queries in many cases are denormalized data projections prepared either by the same data source (SQL views in relational databases) or by different data source that is read optimized (read replica) that is periodically synced with the main source (notice that period can also cause data staleness, so the acceptable ranges should be defined).

CQRS diagram depicting backend side pattern implementation with command and query segregation. UI on top connecting arrows to and from API. API invokes CommandHandler by passing command and QueryHandler by passing query. CommandHandler response with error/status to API or Publishes event to EventHandler. QueryHandler response to API by sending data. Command can write to database (table). While EventHandler updates the database (table). QueryHandler fetches data from database via View (database projection).
CQRS diagram depicting backend side pattern implementation with command and query segregation

Event Sourcing

Another option that fits quite well with CQRS is Event Sourcing pattern. Once a command is successfully handled, the system would publish an event accompanied with sufficient data to indicate the completion and notify other subscribers. Event Sourcing extends on the idea: with the sufficient events aggregated we can always represent the current state of the system.

To provide such ability Event Sourcing pattern is always accompanied with Event Store database. Such data store that provides both document and event-based storages. Document-based storage is used for storing query projections while event-based storage is used for storing the event records (side effects of commands). From queries perspective, we can benefit from the most recent changes by querying event-based store directly or choose to use projections in cases where data staleness is more relaxed (can be stale if the document projections are asynchronously updated on every event change instead synchronously).

Although, understanding data stores is big factor in CQRS architecture, the frontend part should not be overlooked. Frontend, as well as UI/UX, needs to be complemental as a part of the whole architecture.

Frontend Patterns

Now that we have established CQRS as our architectural choice for backend application, let’s look at the approach that is beneficial for the frontend side.

In the near past, most of CQRS implementations tended to set queries operations as near to the web tier in order to benefit from lesser latency and from caching server requests. This would still be preferable option for the most of the server side rendered applications. But, modern frontend frameworks and libraries brought their own caching strategies (e.g. state managers) that benefit us to react more quickly, and easily change parts of UI. Note that having response caches on both ends - frontend and backend side, would cause a lot of issues and misalignments in maintaining consistency. To make it more clear, from know on, we’ll focus on what is the most convenient option that React (and similar libraries/frameworks) can offer us in terms of caching for SPA applications and keep server side caching out of the scope.

Tanstack

Instead of waiting for a user interaction to activate another rendering and refresh stale data, the libraries such as TanStack React Query can help us maintain data staleness by tracking the success of commands and fetching data from queries.

In it’s essence, React Query is a wrapper around HTTP client that provides response caching and methods on how/when to invalidate some caching record and refetch fresh data. It is implemented using Observer pattern where any React component can be a subscriber to cache topics (i.e. cache keys). Any cached record is part of in-memory key/value store where it remains until invalidation occurs due to time expiration or explicit code invocation.

Pattern: Invalidate and Refetch

Lets say we performed AddFamilyMemberContact command and within that altered patient details. As a result, we would prefer to invalidate cache record with the key “patient:0087”. After invalidation, the React Query observer can notify components to rerender with re-fetched fresh patient’s data.

// +++++++++++++++++++++++++++++++++
// ++ Patient's details component ++
// +++++++++++++++++++++++++++++++++
const { data: patient } = useQuery({
  queryKey: ['patient', id],
  queryFn: getPatientDetails,
})

// ++++++++++++++++++++++++++++++
// ++ Patient's edit component ++
// ++++++++++++++++++++++++++++++
const mutation = useMutation({
  mutationFn: addFamilyMemberContact,
  onSuccess: (patientId) => {
    // Invalidate and refetch
    queryClient.invalidateQueries({ queryKey: ['patient', patientId] })
  },
})
Code snippet demonstrating query use for details component and mutation for command side

Pattern: Notify and Invalidate

When it comes to use cases where multiple users collaborate on the same data records, it could be imperative that the latest changes are display at the near real time. If this is the case, we can leverage on web sockets and corelate their events to proper query key invalidation. The following code snippets demonstrate the case where any event received about changes on patient’s document will cause the cache invalidation, so the latest data can be fetched.

// ++++++++++++++++++++++++++++++++
// ++ Patient's notification hub ++
// ++++++++++++++++++++++++++++++++
export function usePatientHub({
  patientId,
  userId,
  onPatientDetailsChanged
}: UseSignalROptions) {

  const connectionRef = useRef<HubConnection | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  const onPatientDetailsChangedRef = useRef(onPatientDetailsChanged);
  onPatientDetailsChangedRef.current = onPatientDetailsChanged;

  useEffect(() => {
    const connection = new HubConnectionBuilder()
      .withUrl("/hubs/patient")
      .withAutomaticReconnect()
      .build();

    connectionRef.current = connection;

    connection.on("PatientDetailsChanged", (msg: PatientDetailsChangedMessage) => {
      onPatientDetailsChangedRef.current(msg);
    });
  }, [patientDetailsId, userId]);
}

// ++++++++++++++++++++++++++++++++++
// ++ Patient's details component ++
// ++++++++++++++++++++++++++++++++++

// ── SignalR: real-time events ──
const { isConnected } = useSignalR({
  patientId,
  userId,

  onDetailsChanged: useCallback(
    (msg: PatientDetailsChangedMessage) => {
      if (msg.editedBy !== userId) {
        // ── invalidate query cache so it re-fetches 
        // the latest details from the REST API.──
        queryClient.invalidateQueries({
          queryKey: ['patient', patientId],
        });
      }
    },
    [userId, queryClient]
  )
});

// ── Save details changes handler ──
const mutation = useMutation({
  mutationFn: (body: UpdatePatientDetailsRequest) => updatePatientDetails(id, body),
});
//...
const handleSave = async (request: UpdatePatientDetailsRequest) => {
  await mutation.mutateAsync(request);
};
Code snippet demonstrating use of web sockets (SignalR) and query invalidation

Pattern: Prefetching

You can track user attention in order to prepare query result by prefetching the latest data. If a user hovers over an HTML element (e.g. a button, link, tab etc.), we can use such information to fire a prefetch API call. At the time of rendering the component will already use a cached response.

// +++++++++++++++++++++++++++++++++
// ++ Patient list item component ++
// +++++++++++++++++++++++++++++++++
const handlePrefetch = useCallback(
  (id) => {
    queryClient.prefetchQuery({
      queryKey: ['patient', id],
      queryFn: () => fetchPatientDetails(id),
      staleTime: 5_000, // don't re-prefetch if already fresh
    });
  },
  [queryClient]
);

//...

return (
<div>
  <button key={patient.id}
          onMouseEnter={() => handlePrefetch(patient.id)}
          onFocus={() => handlePrefetch(patient.id)}
          onClick={() => onClick(patient.id)}>
    Details
  </button>
</div>);
Code snippet demonstrating use of on hover event and quary prefetching

Pattern: Manual Cache Update

In cases when we deal with creation of a new record, sometimes it can be convenient to manually add a new record to the cache upon successful confirmation with response - HTTP 200 OK. Instead of invalidating the cache, which would cause another round trip to the server side, we can make action more responsive without unnecessary rerendering. The usual use case can be a select or dropdown with a lot of items or search list.

Drop down component displaying list of groups with their names. Add button is on the the top of the list.
Drop down with infinite scroll and Add new record button

// +++++++++++++++++++++++++++++++++++++++
// ++ Doctor's create modal form dialog ++
// +++++++++++++++++++++++++++++++++++++++
const handleSubmit = (values: CreateDoctorRequest) => {
  mutate(values, {
    onSuccess: () => {
      const newDoctor = { id: addedItemId, fullName: values.fullName };
      queryClient.setQueryData(['doctors'], newDoctor);
    }
  });
};
Code snippet demonstrating use of manual insert to cache

Pattern: Paginated and Infinite Queries

In cases where the application uses paginated views or infinite scroll, it would be beneficial to refrain from frequent fetching and leverage more on cached data from previously visited pages. This is easily achieved by keeping the placeholder data between re-fetches. It also helps that the page is not in loading state during fetch, but instead previewing stale data until fresh page response arrives.

// ++++++++++++++++++++++++++++++++++++++++++++++
// ++ Custom base type for data table response ++
// ++++++++++++++++++++++++++++++++++++++++++++++
export type DataTableQueryResponse<T> = {
  items: Array<T>;
  totalItemCount: number;
  pageCount: number;
  pageSize: number;
  count: number;
  hasPreviousPage: boolean;
  hasNextPage: boolean;
  isFirstPage: boolean;
  isLastPage: boolean;
  firstItemOnPage: number;
  lastItemOnPage: number;
}

// +++++++++++++++++++++++++
// ++ Patients data table ++
// +++++++++++++++++++++++++
const {data, isLoading } = 
 useQuery<DataTableQueryResponse<PatientItemResponse>>({
      placeholderData: keepPreviousData,
      queryKey: ["patients", fetchURL],
      queryFn: async () =>
      {
        return await getPatientsListPage(fetchURL);
      }
  });
Code snippet demonstrating use of `placholderData` for paginated queries

Conclusion

It is our belief that implementation following CQRS pattern should not neglect the understanding of frontend part. If anything, both backend and frontend side must remain complementary in implementing CQRS pattern.

Our opinion is that designing API and components in a way of CQRS can lead to smoother and more elegant user experience by fractionating parts to commands/mutations and queries. Additionally, with the shown frontend patterns we can notice that there is no need for a big orchestration of the state management. Instead atomicity and low scope of commands affect only a subsequent of cached data, making code succinct and easy to maintain.

For a more different aspects of usage patterns, we recommend checking the Tanstack Query official documentation.

References