Skip to content
This repository has been archived by the owner on Nov 5, 2022. It is now read-only.

Project that lays the foundation of event sourced applications.

License

Notifications You must be signed in to change notification settings

Unthrottled/event-sourcing-workshop

Repository files navigation

Event Sourced Workshop!

Prerequisites

Running the Application

  1. Start up mongo!

    • If using docker just do docker-compose up -d at the root of this repository (also if you have mongo already running locally skip this)
    • If not using docker, be sure that your installed instance of MongoDB is up and running on port 27017!
  2. Checkout the workshop branch on this repository.

  3. Run the application!

  • For this workshop, we are going to need to run the application under a Spring active profile of local.
  • It is easiest to run in the command line using this command:
./gradlew bootrun -Dspring.profiles.active=local

Note: You do not have to use CLI, if using intellij, just make sure that the Spring Profile is set to local, If using another IDE, just make sure you can run a gradle project and be able to set the Spring Profile argument is set to local eg the parameters on the cli: (-Dspring.profiles.active=local)

Introduction

We are tasked with developing an application that allows us to keep track of all of the members in Pod Supreme.

Requirements!!

  • Pod Supreme cares a lot about how the pod has grown overtime.

    • Which means they like to look back overtime and see how things have changed.
  • In addition they also like not having to save their progress.

    • Every action they take is deliberate, so be sure to capture information as it is put in!
  • Pod Supreme asks that they be able to visualise all of the current members in there pod.

  • Pod Supreme needs to be able to add new members and remove fellow friends that leave leave from the pod.

  • Pod Supreme members also want to be able to personalize their profile by being able to use an avatar of any image type (including animated gifs).

  • Pod Supreme members want others to know how they can be contacted in an event of a question, whether it be by phone or email.

  • Pod Supreme members want others to also know what they are currently interested in an the moment. The want the ability to add and remove interests as time progresses and they change.

Current Application State

The frontend was built out to satisfy the functional portions of the requirements listed above.

All data that is added to the application is done through by using events in the form of a Flux Standard Action or FSA for short Which will be stored in as an event stream at the level of Pod Supreme and at the level of the Pod Member

A FSA maintains this type declaration.

{
  "type": string,
  "payload": any,
  "error": boolean,
  "meta": any
}

Basically, the list of pod members in Pod Supreme can be projected by a distinct event stream and the details of each Pod Member can be projected using unique event stream for each pod member created.

However, only the frontend has been built, all data persistence and projections have not been built yet.

Thankfully, they built all of it out to a REST Contract!

REST Contract details

Pod Level

Pod Member Additions

All pod member additions are handled by POSTing and event to the backend route /api/pod/event

With a request body that looks like this:

{
  "type": "POD_MEMBER_CREATED",
  "payload": {
    "identifier": "17d16ba0-b43f-11e8-a39e-ad0592b82c90"
  },
  "error": false,
  "meta": {}
}

NOTE: The identifier is supplied by the UI

Pod Member Removal

All pod member removals are handled by POSTing and event to the backend route /api/pod/event

With a request body that looks like this:

{
  "type": "POD_MEMBER_DELETED",
  "payload": {
    "identifier": "e9e462c0-a7b8-11e8-9852-dbb438e9e7e6"
  },
  "error": false,
  "meta": {}
}

Note: All events are put in sequentially, ie A DELETED event will not come before a ADDED

List Pod Members

Event streams are great and all, but that abstraction should not really matter to any other service that may want to consume our Pod Information.

With that in mind, when the application first loads in the browser, the UI will first attempt to get a list of all of the pod members that currently are active in Pod Supreme

It will GET this information at /api/pod/members. It is ONLY going to respond with a JSON array:

Content-Type: application/json

With a payload that looks like this:

[{
	"_id": "dffc6470-a712-11e8-b3de-89c3131879b4"
},
{
	"_id": "bc1d6900-9fd3-11e8-b28d-df00e344ef92"
}]

Pod Member Level

Pod Member Information

On the topic of information retrieval, all pod member information is expected to be accessible by GETting it at /api/pod/member/{identifier}/information

Where it returns just content type of application/json.

Expected return value is as follows:

{
    "interests": [
      {
        "id": "25313000-b43f-11e8-a39e-ad0592b82c90",
        "value": "Google Feud"
      }
    ],
    "email": "is.my.c@plotting-against.me",
    "firstName": "A Pet",
    "lastName": "Named Steve",
    "phoneNumber": "1234567890"
}

Pod Member Contact Information

All pod member level contact information is persisted by POSTing an FSA to /api/pod/member/{identifier}/event.

The FSA is expected to look something like this:

{
    "type": "PERSONAL_INFO_CAPTURED",
    "payload": {
      "value": "Named Steve",
      "field": "lastName"
    },
    "error": false,
    "meta": {}
  }

Where the field can be firstName, lastName, email, or phoneNumber.

Pod Member Interests

Creation

All pod member level interest information is persisted by POSTing an FSA to /api/pod/member/{identifier}/event.

The FSA is expected to look something like this:

{
    "type": "INTEREST_CAPTURED",
    "payload": {
      "id": "25313000-b43f-11e8-a39e-ad0592b82c90",
      "value": "Google Feud"
    },
    "error": false,
    "meta": {}
  }

Where the UI creates the identifier

Deletion

All pod member level interest information is persisted by POSTing an FSA to /api/pod/member/{identifier}/event.

The FSA is expected to look something like this:

  {
    "type": "INTEREST_REMOVED",
    "payload": {
      "id": "5a619d90-b445-11e8-88d5-5fe830a9621a",
      "value": "Waterfall Development"
    },
    "error": false,
    "meta": {}
  }

Where the UI maintains the reference the identifier

Avatar Access

Remember when I said that the backend has not been built for data persistence? Well I lied, turns out that some of the REST API has been built out. Those parts are the static content forwarding and Avatar Image Persistence/Retreival.

Where the Workshop Work Begins!

1. Create Minimal REST API

We will have 2 REST controllers.

In PodRestController we will need to put the following:

  • POST /api/pod/event
    • Accepts a String and returns the accepted Event (which is a string) as a Optional eg: Optional<String>
  • GET /api/pod/members
    • Returns a empty Stream<Identifier> remember that the return type must be application/json!

In PodMemberRestController we will need to put the following:

  • POST /api/pod/member/{identifier}/event
    • Accepts a Event and returns the accepted Event as a Optional eg: Optional<Event>
    • Needs to also take advantage of the path variable
  • GET /api/pod/member/{identifier}/information
    • Accepts the path variable and returns an empty `Optional

We well need to fulfill the following before we can move onto the next part.

2. Wire in Services into the REST API

It is really convenient that the PodHandler class has a handy API! Which looks a little something like this:

//Pod Handler
public Stream<Identifier> projectAllPodMembers();
public Optional<Event> savePodMemberEvent(String podMemberIdentifier, Event eventToSave);
public Optional<String> savePodEvent(String eventAsJson);
public Optional<PersonalInformation> projectPersonalInformation(String podMemberIdentifier);

Take the time to match the handler API to the corresponding REST API we created above!

3. Implement Service Methods!

Now comes the fun part! We'll start off easy and work our way up!

Implement these service methods in PodHandler!

  1. public Stream<Identifier> projectAllPodMembers();
  2. public Optional<Event> savePodMemberEvent(String podMemberIdentifier, Event eventToSave);
  3. public Optional<String> savePodEvent(String eventAsJson);
  4. public Optional<PersonalInformation> projectPersonalInformation(String podMemberIdentifier);
    1. Start off by projecting contact information
    2. Second project Interests
    3. Combine both projections
    4. ????
    5. Profit!

Event Stream Examples

Pod Level Event Stream

Content-Type: application/json

[{
	"type": "POD_MEMBER_CREATED",
	"payload": {
		"identifier": "d7c9d570-a7b8-11e8-a8e4-afa47f95a3a1"
	},
	"error": false,
	"meta": {}
},
{
	"type": "POD_MEMBER_CREATED",
	"payload": {
		"identifier": "e9e462c0-a7b8-11e8-9852-dbb438e9e7e6"
	},
	"error": false,
	"meta": {}
},
{
	"type": "POD_MEMBER_CREATED",
	"payload": {
		"identifier": "17d16ba0-b43f-11e8-a39e-ad0592b82c90"
	},
	"error": false,
	"meta": {}
},
{
	"type": "POD_MEMBER_DELETED",
	"payload": {
		"identifier": "e9e462c0-a7b8-11e8-9852-dbb438e9e7e6"
	},
	"error": false,
	"meta": {}
}]

Pod Member Level Event Stream

Content-Type: application/json
[{
	"type": "PERSONAL_INFO_CAPTURED",
	"payload": {
		"value": "Party",
		"field": "firstName"
	},
	"error": false,
	"meta": {}
},
{
	"type": "PERSONAL_INFO_CAPTURED",
	"payload": {
		"value": "Parrot",
		"field": "lastName"
	},
	"error": false,
	"meta": {}
},
{
	"type": "PERSONAL_INFO_CAPTURED",
	"payload": {
		"value": "party@parrot.io",
		"field": "email"
	},
	"error": false,
	"meta": {}
},
{
	"type": "PERSONAL_INFO_CAPTURED",
	"payload": {
		"value": "1234567890",
		"field": "phoneNumber"
	},
	"error": false,
	"meta": {}
},
{
	"type": "INTEREST_CAPTURED",
	"payload": {
		"id": "10747c60-b446-11e8-88d5-5fe830a9621a",
		"value": "Party"
	},
	"error": false,
	"meta": {}
},
{
	"type": "INTEREST_CAPTURED",
	"payload": {
		"id": "12b91530-b446-11e8-88d5-5fe830a9621a",
		"value": "Parrot"
	},
	"error": false,
	"meta": {}
},
{
	"type": "INTEREST_CAPTURED",
	"payload": {
		"id": "16463390-b446-11e8-88d5-5fe830a9621a",
		"value": "Not Partying"
	},
	"error": false,
	"meta": {}
},
{
	"type": "INTEREST_CAPTURED",
	"payload": {
		"id": "14040bc0-b446-11e8-88d5-5fe830a9621a",
		"value": "RGB"
	},
	"error": false,
	"meta": {}
},
{
	"type": "INTEREST_REMOVED",
	"payload": {
		"id": "16463390-b446-11e8-88d5-5fe830a9621a",
		"value": "Not Partying"
	},
	"error": false,
	"meta": {}
},
{
	"type": "PERSONAL_INFO_CAPTURED",
	"payload": {
		"value": "Ultra Fast Party",
		"field": "firstName"
	},
	"error": false,
	"meta": {}
},
{
	"type": "PERSONAL_INFO_CAPTURED",
	"payload": {
		"value": "party@parrot.io",
		"field": "email"
	},
	"error": false,
	"meta": {}
},
{
	"type": "AVATAR_UPLOADED",
	"payload": {
		"identifier": "5b953e0dd99cc7703eef3f40"
	},
	"error": false,
	"meta": {}
}]