-1

Context

Domain

I have a CQRS domain with requests, authorizers, handlers, response objects, entities, filters, pagination.

It only depends on:

  • external services (email, other APIs, etc...)
  • a database/persistence layer
  • a way to retrieve the current domain user logged in

Web API

This CQRS domain is called from a web API that exposes 2 things:

  • endpoints for reading files
  • an endpoint to access a GraphQL schema

GraphQL

The GraphQL resolvers and controller actions only send requests to the CQRS domain. Each request represents an atomic use case (e.g. "I want to get the invoice PDF" or "I want to create a new invoice").

So my controllers and GraphQL resolvers are very thin. Great.

The problem

Many problems arise though:

  • I've created a bloated code base because almost every GraphQL type has a corresponding domain type and its corresponding mapping profile (assuming Automapper)
  • The code has increased of about 30% - 40% overall and it's difficult to follow the flow of data
  • All this mapping is extremely error prone
  • I often need to map back from CQRS requests to GraphQL inputs to determine the original input field path of the error that was returned by the CQRS handler/validator

Question

On the one hand it's great to have a common request -> handler -> response pipeline for all of your use cases so that you can implement cross cutting concerns and enforce them at the beginning of the pipeline, on the other hand I've lost the ability to clearly follow through the flow of control.

What am I missing? How is a bloated and complex code base better than a simple straightforward "business logic in controllers / graphql resolvers" logic? Isn't all this mapping a violation of DRY (for example adding a field to an entity requires at least 3 classes to be modified)?

  • Most problemen have multiple solutions, each with their own pros and cons. Your question reads as if you’ve implemented some technology, but you weren’t aware of the problem you were trying to solve and the pros and cons of the technology. Can you explain why you’re using cqrs and graphql? – Rik D Jul 18 at 17:35
  • @RikD I have actually only implemented a very focused subsystem of the overall application to test architectures out. The reasons why I chose GraphQL cannot be explained in comments and seem out of topic. The short story is that I think I can prove that GraphQL is strictly superior to raw REST (no OData or other extensions) (yes REST is not as defined of a term as GraphQL is, but I'm sure you know what I mean) except for some performance enhancement you get from using HTTP verbs and semantic. And those HTTP verbs and semantic can also be used in a GraphQL server if you really want to. – Shoe Diamente Jul 19 at 8:14
  • @RikD I've used MediatR mostly because it helps to organise use cases in different classes as opposed to grouping functionality per "aggregate?" like I've seen in services or repositories. Moreover it has the ability to enhance the request -> handler -> response pipeline with behaviors either specific to a request or general to all requests. It also has the ability to split certain business logic that is not required to be atomic within a certain use case and can instead be regarded as a response to an event (sending an email in response to certain actions for example). – Shoe Diamente Jul 19 at 8:18
  • @RikD Finally making every use case a request allows you to use libraries like FluentValidation to enforce the validation of all requests by default (for example by requiring an IValidator<TRequest> in a behavior). Yes, I know this is not CQRS per se (I don't really split commands from queries) and perhaps that was a wrong choice for words. – Shoe Diamente Jul 19 at 8:19
3

Software engineering is always a series of trade-offs. You have to give up something to get something else. The unfortunate reality is that we don't always have a full picture of what the impacts are on our system when we start. You've mentioned many principles here, so it makes sense to summarize the idea behind it.

Don't Repeat Yourself (DRY)

Bottom line is that if you need to change they way an application behaves, you should know the one place you need to go and fix it there. That's the general idea at least. If you've ever maintained legacy code, you'll find all kinds of duplication which means you need to fix things in multiple places.

There's a limit to where DRY is useful. Sometimes you have to repeat yourself for real reasons that may be out of your control, or dictated by another pattern that addresses a different problem.

Command/Query Responsibility Separation (CQRS)

This is something that can be overused, but it is necessary in some situations. Let's take the idea of blog hosting software. In this situation, the number of reads will vastly outnumber the updates (ideally, at least). By separating the responsibility of finding and reading blog entries from creating them or managing them, you can scale those responsibilities separately.

It's also not uncommon for some updates to be very focused. For example, external systems updating status, etc. The command responsibility does not have to look exactly like the query response. It does increase complexity, so make sure the complexity is worth it.

GraphQL

GraphQL is a tool to map data form disparate sources and massage it into the size and shape the downstream system asked for. GraphQL is it's own language. The queries and responses vaguely resemble JSON, but they wouldn't be valid JSON objects. As such there will always be a translation issue there.

The killer feature of GraphQL is that it allows you to minimize the chattiness with your client over the WAN, while you can federate to different services inside your system boundaries where bandwidth is much more open.

When things don't line up

It's not uncommon for the response object from a rest service not to map directly to the schema for GraphQL. The truth is that they have different languages, so you can't share common code. Additionally, your REST service might be implemented in another language from the GraphQL layer.

Every time you transition between languages, you lose the opportunity to reuse objects. Adding support for a new field does require changes to every step in the chain that represents that object.

For small systems with limited needs to scale, you can argue that the complexity you gained from this process is overkill. You'd also probably be right. However, for large systems with a lot of traffic, that complexity is not only warranted, it is needed.

Think about it this way, with a heterogeneous microservices architecture where nothing is shared between services, you gain some modularity and freedom you wouldn't have otherwise. That allows the team that has to support the new field to ramp up and implement it correctly before the rest of your system is ready to consume that new field. As each system now starts making use of it, you have the opportunity to fix integration problems before making it public through GraphQL.

Bottom Line

Always re-evaluate what you are doing as you progress.

  • What are my pain points, and is there anything I can do about them?
  • Is the general approach good, necessary, or correct for your application?
  • Are there things I can do to minimize the pain without sacrificing the good?

Even if you feel like you went down the wrong path entirely, it always helps to understand how you got to where you are. Those choices you took need to inform your new choices, particularly the pain introduced by those choices.

Sometimes there just aren't any elegant answers. At least, not yet. It takes some time looking at the problems you are trying to solve and playing the "what if?" game to start to figure out ways of taming the beast you've created. That's OK. Software development is also a team endeavor. Someone on your team who is familiar with your domain may have a breakthrough idea to fix it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.