A service that exposes a REST API which allows to create, update, delete and retrieve answers as key-value pairs.
An answer
can be defined as:
key: string
value: string
e.g. in JSON:
{
"key": "name",
"value": "John"
}
The API exposes the following endpoints:
POST /answers
- Create answerPATCH /answers
- Update answerGET /answers/{key}
- Get answer (returns the latest answer for the given key)DELETE /answers/{key}
- Delete answerGET /answers/{key}/history
- Get history for given key (returns an array of events in chronological order)
An event
can be defined as:
event: string
data: answer
e.g. in JSON:
{
"event": "create",
"data": {
"key": "name",
"value": "John"
}
}
If a user saves the same key multiple times (using update), every answer should be saved. When retrieving an answer, it should return the latest answer.
If a user tries to create an answer that already exists - the request should fail and an adequate message or code should be returned.
If an answer doesn't exist or has been deleted, an adequate message or code should be returned.
When returning history, only mutating events (create, update, delete) should be returned. The "get" events should not be recorded.
It is possible to create a key after it has been deleted. However, it is not possible to update a deleted key. For example the following event sequences are allowed:
create → delete → create → update
create → update → delete → create → update
However, the following should not be allowed:
create → delete → update
create → create
Design Patterns used:
Services:
- EventStore
- Projection
- Projector
The EventStore service represents the write side of the application.
The Projection service represents the read side of the application.
The Projector service synchronizes read and write data.
Databases used:
- EventStoreDB as event store
- MongoDB as read repository
Create a .env
file with the following variables:
EVENTSTORE_HOST=localhost
MONGODB_HOST=localhost
MONGODB_USER=root
MONGODB_PASS=example
$ docker-compose up -d
Notice the difference in the port being used:
localhost:8080
for the event store servicelocalhost:8081
for the projection service
$ curl -X POST -d '{
"key":"name",
"value":"john"
}' http://localhost:8080/answers
Successful Response:
{
"ok": "AnswerCreatedEvent"
}
$ curl http://localhost:8081/answers/name
Response:
{
"key": "name",
"value": "john"
}
$ curl -X POST -d '{
"key":"name",
"value":"john"
}' http://localhost:8080/answers
Error:
{
"error": "Key exists"
}
$ curl http://localhost:8080/answers/name/history
Response:
[
{
"type": "AnswerCreatedEvent",
"data": {
"key": "name",
"value": "john"
}
}
]
$ curl -X PATCH -d '{
"key":"name",
"value":"jack"
}' http://localhost:8080/answers
Response:
{
"ok": "AnswerUpdatedEvent"
}
$ curl http://localhost:8081/answers/name
Response:
{
"key": "name",
"value": "jack"
}
Requires to define the environment variables.
$ docker-compose up -d mongo eventstore.db
Find the EventStoreDB dashboard at http://localhost:2113/.
Mongo cli:
$ mongo --host localhost --port 27017 -u root -p example
---
> use qaservice
> db.answers.find()
As a monolith / single-process:
$ godotenv go run ./monolith/.
As separate processes:
$ godotenv go run ./eventstore/cmd/.
$ godotenv go run ./projection/cmd/.
$ godotenv go run ./projector/cmd/.
It applies only to single process running mode.
Use -memdb
parameter to run the services with in-memory db.
$ IP_PORT=:8080 go run ./monolith/. -memdb
$ docker-compose build
$ docker-compose up -d
Clean test-cache (optional) :
$ go clean -testcache
Run all tests (requires databases to be up):
$ godotenv go test ./...
Skip tests flagged as "short":
$ godotenv go test -short ./...
Works on Minikube.
$ minikube start
To keep things simple, in the following workflow pods use databases running on host.
Pods use host.minikube.internal
as address to access dbs on host.
https://minikube.sigs.k8s.io/docs/handbook/host-access/
$ docker-compose up -d mongo eventstore.db
For building docker images directly inside minikube:
$ eval $(minikube -p minikube docker-env)
Tip 2: Evaluating the docker-env is only valid for the current terminal. By closing the terminal, you will go back to using your own system’s docker daemon.
$ docker build -t qaservice-eventstore:1.0 --build-arg="SERVICE=eventstore" .
$ docker build -t qaservice-projection:1.0 --build-arg="SERVICE=projection" .
$ docker build -t qaservice-projector:1.0 --build-arg="SERVICE=projector" .
$ kubectl apply -f kubernetes-deployment.yml
For each service do:
docker build -t qaservice-<service> --build-arg="SERVICE=<service>" .
docker tag qaservice-$service <registry handle>/qaservice-<service>:1.0
docker push <registry handle>/qaservice-<service>:1.0
kubectl apply -f kubernetes-deployment.yml
Solution to ImagePullBackOff error:
minikube image load qaservice-<service>:1.0
Services address:
-
Event store:
$(minikube service qaservice-eventstore --url)
-
Projection:
$(minikube service qaservice-projection --url)
POST example:
$ curl -X POST -d '{
"key":"name",
"value":"john"
}' $(minikube service qaservice-eventstore --url)/answers
GET example:
$ curl $(minikube service qaservice-projection --url)/answers/name