One of the great things about this library is that it can be deployed to serverless environments. It's super small footprint makes it lightning fast to load, even on cold starts.
The basic use case is simple: we simply need to forward request from some API to our read/write models.
Or we can use GraphQL, which works really nicely with our setup. Mutations map directly to the write model and Queries to the read model.
const { GraphQLServer } =require('graphql-yoga')const {readModel,writeModel,} =require('./src')consttypeDefs=` type Todo { title: String! completed: Boolean } type TodoList { id: ID! todos: [Todo] } type Query { getById(id: ID!): TodoList } type Mutation { addTodo(id: ID!, title: String!): Boolean }`constresolvers= { Mutation: {addTodo: (_, { id, title }) => {returnwriteModel.addTodo(id, { title }) }, }, Query: {getById: (_, { id }) => {returnreadModel.getById({ id }) }, },}constserver=newGraphQLServer({ typeDefs, resolvers })server.start(() =>console.log('Server is running on localhost:4000'))
We're really not doing very much in either of these examples, we simply forward requests from the API to our read or write model. In the case of GraphQL, it's a bit more explicit.
(you’ll also need AWS credentials in your local environment)
Now replace the contents of serverless.yml with this:
service:my-serverless-cqrs-service## let's define some constants to use later in this filecustom:writeModelTableName:commitsByEntityIdAndVersionwriteModelIndexName:indexByEntityNameAndCommitIdreadModelDomainName:readmodelprovider:name:awsruntime:nodejs8.10## make these values available in process.envenvironment:WRITE_MODEL_TABLENAME:${self:custom.writeModelTableName}WRITE_MODEL_INDEXNAME:${self:custom.writeModelIndexName}ELASTIC_READ_MODEL_ENDPOINT:Fn::GetAtt: - ReadModelProjections - DomainEndpoint## allow our lambda functions to access to following resourcesiamRoleStatements: - Effect:AllowAction: - "dynamodb:*"Resource:"arn:aws:dynamodb:*:*:table/${self:custom.writeModelTableName}*" - Effect:"Allow"Action: - "es:*"Resource: - "arn:aws:es:*:*:domain/${self:custom.readModelDomainName}/*" - Effect:"Allow"Action: - "es:ESHttpGet"Resource: - "*"## Create an API Gateway and connect it to our handler functionfunctions:router:handler:app.handlerevents: - http:ANY / - http:ANY {proxy+}resources:Resources:## create a DynamoDB table for storing eventsEventStoreTable:Type:'AWS::DynamoDB::Table'Properties:TableName:${self:custom.writeModelTableName}AttributeDefinitions: - AttributeName:entityIdAttributeType:S - AttributeName:versionAttributeType:N - AttributeName:entityNameAttributeType:S - AttributeName:commitIdAttributeType:SKeySchema: - AttributeName:entityIdKeyType:HASH - AttributeName:versionKeyType:RANGEProvisionedThroughput:ReadCapacityUnits:5WriteCapacityUnits:5GlobalSecondaryIndexes: - IndexName:${self:custom.writeModelIndexName}KeySchema: - AttributeName:entityNameKeyType:HASH - AttributeName:commitIdKeyType:RANGEProjection:ProjectionType:ALLProvisionedThroughput:ReadCapacityUnits:5WriteCapacityUnits:5## create an ElasticSearch instance for storing read model projectionsReadModelProjections:Type:'AWS::Elasticsearch::Domain'Properties:DomainName:${self:custom.readModelDomainName}ElasticsearchVersion:6.2EBSOptions:EBSEnabled:trueVolumeSize:10VolumeType:gp2ElasticsearchClusterConfig:InstanceType:t2.small.elasticsearch
The AWS Free Tier covers most of these services, but running an ElasticSearch instance on AWS can be expensive. Make sure to run serverless remove once you're done.
Now let's modify the example above to have it work on AWS Lambda
constexpress=require('express')constserverless=require('serverless-http')// ^ add this/// ...same contents as abovemodule.exports.handler =serverless(app)// ^ add this// app.listen(port, () => console.log(`Example app listening on port ${port}!`))// ^ remove this
Examples
The domain in these examples also contain the commands completeTodo and removeTodo.