Hi all,
I’m leading the backend team for a project leveraging CQRS via Spring Boot & Axon. I’m trying to work with the front end team on the preferred way for them to interact with the back end. There are a few options here, IMHO, and I feel like they should be separate for the write versus read sides.
Write side options:
- Option
W1
: REST client →@RestController
→ Strongly Typed Write-Side Interface →CommandGateway
- Option
W2
: Web MQTT client over WebSockets → MQTT broker → MQTT/Axon connector? →CommandGateway
- Option
W3
: Mobile MQTT client → MQTT broker → MQTT/Axon connector? →CommandGateway
- ???
Read side options:
- Option
R1
: REST client →@RestController
→ Strongly Typed Read-Side Interface →QueryGateway
- Option
R2
: GraphQL client → GraphQL server → magical configuration or some Axon/GraphQL module? →QueryGateway
- Option
R3
: GraphQL client → GraphQL server → Read databases (bypassing Axon’sQueryGateway
)
Above, when I say “Strongly Typed … Interface”, I mean the strongly-typed interface I authored for each side.
NB: ignore the Kotlin-specific stuff, like suspend
in the following examples. We are awaiting changes to the Kotlin extensions being discussed here. For now, we have our own Kotlin extensions, but you should get the idea from these examples nonetheless.
For example, on the write side, it’s
// irrelevant package & imports omitted
import kotlinx.coroutines.future.await
import org.axonframework.commandhandling.gateway.CommandGateway
import org.springframework.stereotype.Component
@Component
class CqrsAppointmentWritingService(val cgw: CommandGateway) {
suspend fun bookAppointment(cmd: BookAppointmentCommand) {
return cgw.send<Unit>(cmd).await()
}
suspend fun rescheduleAppointment(cmd: RescheduleAppointmentCommand) {
return cgw.send<Unit>(cmd).await()
}
suspend fun cancelAppointment(cmd: CancelAppointmentCommand) {
return cgw.send<Unit>(cmd).await()
}
suspend fun deleteAppointment(cmd: DeleteAppointmentCommand) {
return cgw.send<Unit>(cmd).await()
}
}
This is basically strongly-typed lipstick over the CommandGateway
, which is what we’re looking to expose to front end clients in various ways as enumerated above. A couple of open questions for me:
- Are there any other ways you might expose this interface besides my
W1
architecture above? - How do I deal with asynchrony when the front end issues a command and is awating some kind of response?
On the read side, same idea: strongly-typed lipstick over QueryGateway
.
// irrelevant package & imports omitted
import kotlinx.coroutines.future.await
import org.axonframework.extensions.kotlin.query
import org.axonframework.extensions.kotlin.queryMany
import org.axonframework.queryhandling.QueryGateway
import org.springframework.stereotype.Component
@Component
class CqrsAppointmentReadingService(val qgw: QueryGateway) {
suspend fun findAppointmentById(q: FindAppointmentByIdQuery): AppointmentDto {
return qgw.query<AppointmentDto, FindAppointmentByIdQuery>(q).await()
}
suspend fun findAppointments(q: FindAppointmentsQuery): List<AppointmentDto> {
return qgw.queryMany<AppointmentDto, FindAppointmentsQuery>(q).await()
}
suspend fun findAgenda(q: FindAgendaQuery): List<AppointmentDto> {
return qgw.queryMany<AppointmentDto, FindAgendaQuery>(q).await()
}
suspend fun findAppointmentSlots(q: FindAppointmentSlotsQuery): List<AppointmentSlotDto> {
return qgw.queryMany<AppointmentSlotDto, FindAppointmentSlotsQuery>(q).await()
}
}
Note architecture R3
above, which bypasses QueryGateway
completely by bolting on a GraphQL server directly to our read databases. Opinions on that are particularly welcome, as both our front end and backend teams see GraphQL as a superior alternative to REST on the read side.
Thanks in advance for your thoughtful input.
–matthew