0:00 Hey everyone. Today I'm super excited to bring you a 0:03 brand new video on 0:04 building a highly scalable audit log system with 0:08 NestJS and EventStoreDB using a pattern 0:11 called event sourcing. Now, in a typical application, we 0:15 have our database that stores the current state of things. 0:19 So for example, in this application, we'll have an accounts table, and 0:23 typically you'd have a balance column. 0:26 When someone withdraws money, you would overwrite that balance number. 0:30 The old value is now gone, and if you want the history, you'd have to 0:34 bolt an audit table on and hope that every code path remembers to 0:38 write to it. Sooner or later, something doesn't, and now your audit 0:42 log disagrees with your data. Event sourcing flips that 0:46 around. Event sourcing flips that around. 0:48 Instead of storing the current state, we are storing every 0:52 change as an immutable event. Account opened, 0:56 money deposited, money withdrawn. 0:59 These events are the source of truth. 1:01 The current balance isn't stored anywhere. 1:03 It's calculated by replaying the events, which means our audit 1:07 log isn't a side feature we add on, it is the database 1:11 itself. We also get some things for free 1:13 that we can't really do 1:15 easily otherwise, like we can rewind to any point in time 1:18 and see 1:19 exactly what the account looked like by simply replaying the 1:22 events up to that point. We can build any number of read models 1:26 off the same history without having to touch the code that writes it. 1:30 Now, to store these events, we're using EventStoreDB, 1:34 which has been renamed to CurrentDB, and it's a database built 1:38 specifically for this pattern. It stores stream of events, it 1:42 guarantees their order, it checks for concurrent writes, 1:45 so two requests 1:46 can't both spend the same money, and it can push every new event 1:50 to subscribers in real time. And the last part is what makes this 1:54 whole thing scalable and what we're going to be using in this 1:56 application 1:57 build-out. Now in this lecture, we're actually gonna be building a NestJS 2:01 backend, a banking application where we can open an 2:04 account, 2:05 deposit, withdraw. So these are actions we can take on the account. 2:09 Every one of these will write an event to CurrentDB, and then we're gonna 2:13 have a background process that will listen to the 2:17 stream of events and project them into a PostgreSQL 2:21 database. So this will be our read-only PostgreSQL 2:25 database where we store our up-to-date calculations of the 2:28 current account state. So I'm super excited to jump in 2:32 and to start 2:32 building with CurrentDB in NestJS. 2:35 So let's go ahead and get started. I'll see you there. 2:38 All right, so to actually go ahead and start our application, I'm gonna be 2:42 using Bun for my projects moving forward, the Bun 2:46 runtime post in Node.js, as it is much faster. 2:50 And personally, I just love the developer experience much 2:52 more. 2:53 Now, NestJS on its latest version has much 2:56 closer support to 2:57 supporting Bun natively. It runs on ESM natively, 3:01 which Bun requires. However, they still don't support it as a 3:04 package manager or runtime inside of their CLI. 3:09 So I've actually created my own CLI that you can use to start new 3:13 NestJS projects with Bun. You can even choose to use 3:17 the Bun HTTP server instead of Express, which is 3:21 faster and more efficient by passing in the Bun adapter. 3:25 However, you don't have to. We can continue using Express, 3:28 which I'll do in 3:29 this lecture as it is more mature. So if you wanna follow along and use Bun, 3:32 just simply copy this command here. 3:34 I'll leave a link to this page if you wanna see more about it. 3:37 Bun create add nestbun with the name of the projects. 3:42 Now, if you don't wanna use Bun and just wanna use pnpm 3:45 or a different package 3:46 manager, you can just use the Nest CLI as usual 3:49 with nest 3:50 new. If you don't have that, you can just install it globally at 3:53 nestjs/cli. However, in our case, like I said, I wanna 3:57 use Bun, the latest and greatest, fastest runtime package manager 4:01 and best developer experience in my opinion. 4:04 So I'm gonna go ahead and call this nestjs-event-sourcing. 4:09 Call it whatever you'd like. 4:11 This is gonna go ahead and ask us which HTTP 4:14 platform we wanna use. Again, you can choose Express here or 4:18 bun.serve, which will use Bun 4:22 HTTP server. 4:23 Let's stick with Express. And you see here, this will go ahead 4:27 and now 4:27 initialize the new project. You can cd into it 4:31 and open it up inside of our code editor. 4:34 All right, so now we have a starting NestJS 4:37 application with 4:38 Nest Bun. 4:40 Now you're gonna see some familiar things here. 4:41 Won't go into everything in a deep dive, but we have our 4:45 package.json with our scripts to start and build the 4:48 application. Notice this is using Bun directly here 4:52 instead of Node.js. And the nice thing about Bun is we can 4:56 execute TypeScript directly, and so everything else should look 5:00 exactly the same. In our source directory, we have the starting NestJS 5:03 structure with our main.ts where we are creating the 5:07 Nest application. We are now using NestJS twelve 5:11 latest validation pipe implementation, which has native Zod 5:15 support. Very nice to see. We can use Zod schemas directly to 5:19 validate incoming requests that conform to a Zod schema. 5:23 And importantly, we are starting the HTTP server here by 5:26 calling 5:26 app.listen and passing through a port or port three thousand. 5:30 So that will start the HTTP server using Express. 5:33 We then have the app module where we declare our 5:36 NestJS HTTP controllers and our injectable 5:40 providers. So we have the starting app controller 5:43 that is 5:44 a simple get route and post route here with the 5:47 injected app service. And we just have some dummy methods now 5:51 that returns tub strings. So this is just the starting NestJS 5:55 application structure that we should remember. 5:59 There's also some other goodies in here, like a CloudMD file to get you up and 6:03 running with Cloud on the project. 6:05 And the latest NestJS linter oxlint. 6:08 So hope this is useful for you to get your projects up and running. 6:12 And then we can run our Bun dev to execute the 6:16 dev package command, which will start up the 6:20 dev server by simply executing main.ts. 6:22 And all we have to do is pass the watch flag, 6:25 and it will automatically reload 6:26 whenever there are changes. So again, a really nice developer experience 6:30 here having this built into Bun. 6:33 Just run Bun dev. That will invoke main.ts 6:36 and start up the HTTP 6:37 server now, where we should now be exposing 6:40 that app controller, that 6:41 single HTTP route with get and post. 6:45 And we can see this working as expected now at localhost:3000, 6:49 Hello World stub string returned back from that controller route. 6:52 So this is great. At this point, we have the NestJS app running 6:55 and ready to go to 6:56 start implementing our event sourcing architecture. 7:00 Now, next up, we're gonna implement a Docker Compose 7:03 file to add 7:04 the external services we need, like CurrentDB and PostgreSQL. 7:08 So I'm gonna go ahead and add this, and then we can quickly walk through it 7:11 and 7:11 then start building from there. All right, so now I've gone ahead and added a 7:15 Docker Compose file, which if you don't already have Docker on your 7:18 machine, 7:19 make sure you go ahead and Google and install Docker Desktop. 7:23 I have Docker Desktop running, which is going to allow us to run Docker 7:26 containers, which are gonna be the underlying services 7:29 that our application 7:30 relies on. So of course, we need CurrentDB, which is gonna be 7:34 the event store DB, where we actually store the 7:38 events on our entities so that we can replay them and 7:41 subscribe to changes as well. If you're not familiar with Docker, 7:45 essentially here we are defining the two services that we 7:48 are gonna run. 7:49 So we have CurrentDB with the Docker image that we're pointing to. 7:53 That's, that's CurrentDB at latest. 7:55 We're also configuring a few environment variables, like to 7:59 enable gRPC connections from CurrentDB 8:03 to our application. CurrentDB uses gRPC 8:07 to connect to our Node app. 8:10 We expose it on port two one one three. 8:13 We're disabling SSL security locally here. 8:18 And finally, allowing for projections to run, which 8:22 essentially just means that we can subscribe to all 8:26 changes on the event store. And we also have a cluster 8:30 size, so CurrentDB can run in distributed mode, 8:33 where we 8:34 can run multiple instances for fault tolerance. 8:37 Here we're just setting it to one for local development. 8:40 We also are defining the port here, two one one three, under the ports 8:44 key. And this is really important because this 8:47 is going to 8:47 expose the gRPC port inside of the CurrentDB 8:51 container to our local machine at port two one one three 8:56 so that we'll be able to access CurrentDB connection on localhost 9:00 two one one three, thanks to this port forwarding here. 9:03 We're also setting up a couple of volumes in order to persist the event 9:07 data that gets stored in the container so it doesn't get removed when it 9:10 stops. And finally, a simple health check to make sure it's 9:13 healthy. 9:15 Down below, we're doing the same thing with a 9:17 PostgreSQL container. 9:18 So we're getting the PostgreSQL 18 image, setting up 9:22 some username, password, and DB environment variables. 9:26 We're also doing our port forwarding here so that we can access this container on 9:29 localhost, a data volume, and health check as well. 9:33 And then all of these volumes are set up on our local machine so 9:36 that the data is 9:37 persisted. So now this is gonna allow us to run these two 9:40 containers 9:41 inside of our application, which we're gonna need to 9:45 actually build everything out. So this is excellent. 9:48 Now, I'm also gonna add a couple of commands to 9:51 our package.json 9:52 just to make this a bit 9:54 easier and more friendly to run. So I've added these three commands to make 9:58 working 9:58 with the containers a bit easier. We have a command to invoke Docker 10:02 Compose, which is the CLI to use Docker Compose. 10:06 We use up to start it. We have a down command, and 10:10 finally, a command here, reset, to actually remove and delete the 10:14 volume so that we can have a clean slate if you wanna remove any of the 10:17 stored data. 10:19 So with these three commands, we should now be able to start up our 10:23 application dependencies. Now, the nice thing about this 10:26 command is it'll actually run in detached mode, so we can just use the 10:31 same terminal session we have. Now run Bun Infra up. 10:35 This will start up both of your containers, pull them 10:38 if you don't have them yet, 10:39 and make sure they are up and running. 10:42 And now we still have access to the terminal. 10:43 So both of those are running in the background now, which is excellent. 10:47 And we can rerun Bun dev now to start up the Bun server. 10:52 And now at this point, we have all of the services 10:55 and dependencies we 10:56 need to actually run the app. This is excellent. 10:59 In the next step, let's make sure that we install the application 11:02 dependencies 11:03 we're gonna need to work with in this project. 11:06 So for this, we are going to use Bun install, 11:08 the Bun package manager, 11:10 and install the dependencies we'll need. 11:12 So that'll be @current/current-db-client, which is how 11:16 we connect to CurrentDB. We're also gonna get drizzle-ORM, which is 11:20 the ORM we'll use to connect to our PostgreSQL database. 11:23 We'll also get the PostgreSQL driver. 11:26 So go ahead and install these. We'll also make sure we install 11:30 -D for development drizzle-kit, which is 11:34 going to allow us to write migrations for our PostgreSQL 11:38 database. All right, so now we have the dependencies we'll 11:40 need. 11:40 We are finally ready to start actually setting up our application, and 11:44 we're finally ready to start actually building our application out. 11:47 And as I said before, this is gonna be a banking example where we 11:51 will have 11:51 bank accounts, and we can have operations on those 11:54 accounts, 11:56 like withdrawing, depositing, and freezing the account, which is 12:00 gonna be an excellent example for this event sourcing because it, again, 12:03 allows us to replay the account at any given point in 12:07 time. And also build a nice projection by 12:11 subscribing to the event source stream and 12:14 building that Postgres read store where we can get quick 12:18 access to 12:20 query across multiple accounts. So let's go ahead and start 12:24 building next. All right, so the first thing we're gonna build 12:26 is the 12:27 event store itself, which will allow us to 12:31 establish a connection to the current DB running 12:35 and actually append and read from these 12:39 underlying streams. Now, streams in event store 12:43 or current DB are essentially the un- logical unit of work. 12:47 You can almost think of them like an RxJS stream, of course, 12:51 but 12:52 maybe more in database terms like the row itself with inside of a 12:56 table. So in this case, we will have a new separate 13:00 stream for each 13:02 individual account in our application, and that's because 13:06 that stream will record all of the relevant events for 13:10 that particular account on that single stream. 13:13 And so we'd have multiple streams for different accounts. 13:16 And so it's these streams that really we're working with inside of 13:19 the code. 13:20 We will be able to replay the history of that stream or that 13:24 particular account. 13:26 And this is again why we need Postgres as the 13:30 secondary read... as the read store here, because when we 13:34 want to actually do actions and read across multiple accounts, 13:38 then we have to 13:39 look across multiple streams, and that can become expensive. 13:43 And so this will become more clear as we start to build it out. 13:47 Let's go ahead and start by cleaning up our source directory, 13:51 removing our 13:53 default app files that we won't need, or remove them from the app 13:57 module. And now we should be up and running and ready 14:01 to go. 14:02 Let's go ahead and start by creating a new 14:06 event store folder. Event store will have the 14:10 event store module and the event store 14:13 service. Let's begin with decorating the module 14:17 with the 14:17 AppModule decorator from Nest Common and export 14:21 class EventStoreModule. Now, I'm also gonna make this 14:25 a global module because it'll be used in multiple areas of our 14:29 application, 14:31 and we only want a single instance, so this means we don't have to re-import 14:34 it 14:34 everywhere that we wanna use it. And I'm critically also going to import 14:38 it now in the app module itself through the 14:42 imports array so that this makes it available to the 14:46 entire Nest application. So now we have the module. 14:49 Let's head back into the event store service where we're gonna 14:53 have an injectable class. So this makes it 14:57 part of the NestJS dependency injection system so that we can 15:00 inject it in other providers later on. 15:03 We will export class EventStoreService that implements, 15:07 importantly, OnModuleDestroy. This is a NestJS 15:11 application lifecycle hook, which just means that we can 15:15 call a function in here whenever the server is shutting 15:18 down to clean up our connection to the current DB 15:22 database. So just a bit of housekeeping 15:25 that will allow us to do this. 15:26 And so now we have the event store service. 15:29 What we wanna do is, of course, firstly establish a connection to 15:33 current DB. So I'm gonna begin with creating 15:37 a private readonly logger 15:40 using new Logger from Nest Common. 15:42 This will allow us to pass in the event store service name 15:45 and log messages to the console with a bit of context. 15:50 And then we're gonna declare the readonly client here. 15:54 That will be the current DB client from 15:57 current/currentDBClient. 16:01 And to establish the connection to the server, we simply call the 16:04 .connectionString method. Now, one of the amazing things 16:08 about using Bun as our runtime now instead of NestJS is that it 16:12 automatically loads in environment variables from a .env 16:16 at the root of the project, 16:18 which is so nice. It means we don't even need to use NestJS 16:21 config or .env under the hood. This all happens for us 16:25 by default with Bun. Again, such a great developer experience. 16:29 However, we still want to encapsulate our 16:32 environment variables 16:33 in a file so that we can apply validation to them. 16:37 So for this, I'm gonna create a new file inside of 16:40 source, 16:41 call it config.ts. And in here, let's go 16:45 ahead and create a new EnvSchema, which will use 16:49 Zod, the schema validation library, 16:52 to define the environment variable schema. 16:55 So here we'll have the object where we define all of the expected 16:59 environment variables. So we can begin with the port for the 17:02 HTTP server. I'm gonna coerce this from a string to 17:06 a number, 17:08 make sure it's a valid integer, and default this to port 17:12 three four five zero if it is not found. 17:15 So Zod, very powerful schema validation library 17:18 that allows us to 17:19 define these validators to enforce 17:22 the structure and even defaults of our data. 17:26 We'll then define that current DB connection string. 17:30 This will be z.string to enforce that it is a string. 17:34 And for the default value, we can provide the connection string of 17:37 current 17:37 DB. That's the protocol, 17:40 localhost:two one one three. Remember, that is 17:44 the port that we are exposing in the Docker compose that 17:48 maps to the current DB internal port where we 17:51 expose a gRPC server so that we can actually connect. 17:56 We'll also pass TLS to false because we're not using TLS 18:00 in this local server. And so this is excellent. 18:02 We now have the environment variables we need. 18:05 We can make sure we export env as 18:09 EnvSchema 18:12 .parse, which will now validate any object 18:16 and make sure it conforms to this schema that we've now defined. 18:20 Well, in this case, that is simply process.env 18:22 that Bun provides here 18:24 by default, which is amazing. So now we have a fully validated 18:28 environment variable. Let's make sure we're using 18:30 that in our main.ts 18:32 file here, where remember, we're listening for an HTTP port. 18:35 We're doing this coercion ourself to a number 18:40 and defaulting, but now we can simply pass in our env 18:43 objects and access the port where we have both of this 18:47 functionality already implemented. So really nice and clean here. 18:52 And now back in the EventStoreService, we'll do the same thing for 18:56 the connection string. This will simply be 18:59 env.CURRENTDB_CONNECTION_STRING. 19:04 So now we have the connection to the underlying 19:08 Current DB database established, which is excellent. 19:12 Let's also add the onModuleDestroy method, which 19:15 adheres the onModuleDestroy interface, which again, this is just 19:19 called whenever the application will shut down. 19:24 So here, let's use the logger and log a message to say, 19:28 "Closing 19:30 Current DB 19:33 client," 19:35 and then clean up our connection by calling await 19:37 this.client.dispose, which closes any 19:41 connections. And we will mark this as async to use 19:45 async await, which NestJS supports. 19:48 And now our EventStoreService is looking good. 19:52 We get also wired into the module 19:55 as a provider, so add EventStoreService 19:59 so that NestJS will register it in the application. 20:03 I will also add it to the exports array because it's gonna be used in 20:07 other areas of the app later on. 20:10 And now we want to actually implement the ability to read 20:14 and append, uh, new events to given streams 20:18 in the EventStoreDB. So let's go ahead and see how we can do this 20:21 next. All right, so now in the EventStoreService, 20:24 let's begin with 20:25 adding our first method, async append, which 20:29 is going to actually add a new event to a 20:33 given stream inside of our EventStoreDB. 20:36 And so append is gonna take in a few parameters. 20:39 The first will be the stream name, which is the, again, logical 20:43 unit of work inside of EventStoreDB. 20:46 You can think of this like the row inside of a database 20:50 table. For example, we'll have a different stream for each 20:53 individual 20:54 account because the events that occur on 20:58 a given 20:59 account together will make up the stream. 21:02 We can replay and build a history of a single 21:06 account at any time by walking back or forward the 21:09 events on this stream for a given account. 21:13 Next up, we have the underlying events. 21:15 These are gonna be the array of events that we want to 21:19 add on to this stream inside of the EventStoreDB. 21:23 And so for this, we're gonna go ahead and define a new type at the top of our 21:27 service, export interface NewEvent. 21:31 This will take in a type generic T that will extend 21:34 string defaulting to string. 21:38 This is gonna be the type name, so essentially the name of the event 21:42 itself will be the type. 21:45 Data will be a generic record with a key 21:49 string and an unknown value, 21:52 and then the metadata associated with the event. 21:55 So we'll have associated metadata for each particular event, 22:00 taking in the same shape of string unknown. 22:04 Now we have the shape of what a new event will look like on our streams. 22:08 We can use this type NewEvent array. 22:12 And then finally, we're gonna have a type called 22:15 ExpectedState. Now, what 22:19 ExpectedState is, is essentially a record of 22:23 the very last event written to the current stream. 22:27 So inside of this stream that we are working with, in this case for a 22:31 single account, each event is gonna have its 22:35 own event identifier, which is simply just a number. 22:39 So this will begin at one and increment onwards as we add new events 22:43 to the stream, and the ExpectedState here is the very 22:47 last event number written to the stream. 22:50 Now, the reason why we want this is to essentially implement 22:54 optimistic locking around concurrency within the EventStore 22:57 stream. Because we have, for example, a distributed 23:01 application where we might have multiple services appending to the 23:05 same event stream, and we wanna make sure before we add 23:09 on any new events to this stream that we are currently working 23:12 with the latest version of it. If we add on an event to the 23:16 stream after another instance of the app has already done so, we might be in 23:20 an inconsistent state. So EventStoreDB solves this 23:24 for us by ensuring optimistic locking, where we 23:28 provide it with the latest event identifier on the stream, 23:32 and it checks that against what actually exists. 23:36 If they don't align, then we will throw a version error that 23:40 allows us to f- handle it based on our application needs. 23:44 We can go ahead and then retry to rewrite the record 23:48 now that we know the latest state, 23:50 or we can ask the caller to retry the request, whatever 23:54 fits our needs best. And so by having this functionality, 23:57 we always 23:58 ensure our stream is in a consistent 24:01 state free of any corruption due to concurrency control, which is 24:05 a big benefit to EventStoreDB offering this out of the 24:09 box. So the type we're gonna use here is actually provided for us 24:13 inside of EventStore. It's called 24:15 AppendStreamState. So this comes from 24:18 current.currentDBclient. And this again is just going to be the 24:22 integer identifier declaring the latest event that 24:26 we know of on this given stream. And that is the caller's 24:29 responsibility to give that to us because it will have its own 24:33 in-memory stream at that time when it wants to write. 24:37 We're going to return back a promise of type append results, 24:42 again from CurrentDB Client, and that's going to be the result of 24:45 appending 24:46 to the stream. 24:47 And so now at this point, we are ready to actually do the work of 24:50 appending to 24:51 this given stream. Now from here, we're going to go ahead and 24:55 create our payload, which is what we're going to insert into the 24:59 stream. Now this is going to be JSON data 25:02 that we're going to be inserting 25:03 in the CurrentDB. 25:06 So in order to convert this into the JSON data type that CurrentDB 25:09 expects, 25:11 we will map over each entry in the array. 25:14 And for each one, we're going to return the JSON event type 25:18 from CurrentDB Client. This takes in a 25:21 JavaScript object that will be converted into the 25:25 JSON type expected for the CurrentDB. 25:29 Here we need to provide it with the underlying data for the event. 25:33 So that includes the type of the event, which is a 25:36 CurrentDB type. 25:39 This is uniform data that all events in CurrentDB will share. 25:44 So the type is the event name, essentially the identifier for it. 25:47 The data is the underlying data for the event. 25:50 And then finally, we have metadata about the event itself. 25:54 And so we pass those through all from our custom new event type. 26:00 And now we've successfully converted each one into a JSON 26:04 event. What I want to do next is open up a try-catch 26:07 statement where we'll catch any errors as a result of the insert. 26:12 We're going to return await a call to 26:15 this.client.appendtostream, 26:20 which is how we can add on new events to a stream or create it 26:24 if it doesn't exist. So to do this, as we can see, we need to pass in 26:28 the stream name, 26:30 which we can do from our parameter, 26:33 the underlying payload, which now is the JSON event array 26:37 that we've converted. And then finally, this takes in an options object 26:41 where you can pass through the stream state. 26:44 So if we don't pass this through, we won't get this concurrency 26:48 control, this optimistic locking we talked about. 26:51 But now since we're passing through this stream state parameter 26:55 as the expected state, again, the last known 26:58 event in the stream that we're working with, this means 27:02 that if the 27:02 expected state here does not match the actual latest event on this 27:06 stream, it will be rejected because some other 27:09 instance may have 27:10 already written to the stream. 27:12 Otherwise, we will add the events to the stream here 27:15 and that will be 27:16 successful, which is excellent. 27:19 However, now we want to handle this potential error 27:22 case if 27:23 we did try to append to the stream, but something else had written to it 27:26 before. 27:27 To do this, we can check the type of the error thrown 27:30 here and check to 27:31 see if it is instance of wrong expected version. 27:35 This should be wrong expected version error from the 27:39 CurrentDB client. 27:41 So if this error is instance of this type, 27:45 then I want to go ahead and throw a new custom error type in our 27:48 application that we'll handle later on in a common NestJS 27:52 filter, which allows us to handle all failed 27:55 requests at the 27:56 HTTP level. So we'll create our own error type here 28:00 called a concurrency error. This will be a standard error in 28:04 our application that extends the existing error type. 28:07 The constructor will take in the error details. 28:10 So the stream name where it occurred. 28:14 Read only expected is the append stream states where we 28:18 last expected the stream event cursor to be. 28:21 And finally, the read only actual, which is the actual state. 28:25 So now we're going to pass through a super call and say 28:29 concurrency conflict on 28:32 stream name 28:35 colon expected string expected. So turn this 28:39 into an output value 28:41 and say actual 28:43 string actual 28:46 to actually see the latest event that was tried that actually 28:50 exists versus what we passed in. 28:53 I also want to set the name of this error for better stack 28:56 traces to be a concurrency error. And now when we handle 29:00 this error type at our filter level, we'll be able to extract this error 29:04 message and send it back to the user for a nice user friendly error 29:08 message. However, we will actually catch this type 29:12 higher up in our application when we actually call this method 29:15 and handle 29:16 it to implement some retries because we know we can 29:20 retry this operation with the latest 29:23 expected stream state and it will succeed. So that's what we will do later on. 29:28 And that's why we want this common error type so that we can handle this. 29:32 So we'll throw a new concurrency error now, pass through the stream name, 29:37 error.expectedState and 29:40 error.actualState. 29:43 Make sure this error type here is correct. 29:46 And you can see since we have this type guard, these are all the properties we 29:50 have on the wrong expected version error from 29:54 CurrentDB. So they give us the expected state 29:57 and the actual 29:58 state when this error does occur. 30:00 Otherwise, we're going to rethrow the error to the 30:03 caller. 30:04 And now we have a fully working append method to add new events on our 30:08 CurrentDB event stream, which is excellent. 30:12 Next up, let's look how we can support both reads 30:14 and 30:15 subscriptions to the underlying event streams. 30:19 All right. So now that we have our append method 30:22 implemented, 30:23 let's go ahead and add a method to actually now read from 30:27 an event stream. So in order to do this, we are going to use a 30:30 generator function. 30:32 Which in JavaScript is essentially just a way to write a 30:36 method where we still use async and for await. 30:40 However, when we call this method, we return straight away, 30:44 and we use a special keyword called yield inside of the method to 30:48 send one value to the consumer and then pause the function until the 30:51 consumer asks for the next one. So it's sort of a similar paradigm 30:55 and way to think about handling asynchronous processing in an 30:59 application. So to mark this as a generator function, 31:02 we use the 31:02 asterisk, and we'll call this read. 31:05 So to take in the parameters, we will accept stream name, which 31:08 is the name of the 31:09 stream we're trying to read. We are then gonna also offer 31:12 underlying options for this method call. 31:15 We will allow the from revision to be provided, which will 31:19 be a big int. So essentially, we're allowing consumers to 31:23 choose where on the stream they want to read from, and this is called 31:27 the revision or again, this is the event state, the number that 31:31 identifies the event based on when it was inserted onto the 31:34 stream. We'll also allow for a max count 31:38 if we want 31:38 to limit the amount of records that we pull back. 31:41 Default to an empty object. So for the return type, let's 31:45 mark this as an async generator, 31:50 which is the return type from an async generator function. 31:54 Then we return the type as a type generic as to what we 31:57 return when we yield from the method. 32:00 So here, this is gonna be of type JSON recorded 32:03 event from 32:04 CurrentDB, which is gonna be the event returned back 32:07 in a JSON 32:08 format. So we'll see what that looks like as we work with it. 32:11 Now, the first step is to create our iterator, which 32:15 is going to allow us to open... Essentially, you can think of like a 32:19 JavaScript stream. However, it's not gonna be the same thing as an 32:22 observable stream. It is going to be an async iterator. 32:26 Now, this iterator is an object that gives us values one at a time 32:30 when we ask for them. And this is really the beauty of 32:34 async generator functions, is they allow us to control the flow of 32:38 execution of the method directly inside. 32:42 And so to create this iterator, we're going to use the CurrentDB client 32:45 object read stream method 32:49 that takes in the name of the underlying stream that we're trying to read. 32:53 And again, that's gonna be identified by the account 32:55 ID in our application. 32:57 We're gonna have a separate stream for each account. 32:59 We can then pass through the from revision 33:03 from our options, which make sure we label correctly. 33:07 So we have options.fromrevision, which again is where we 33:11 want to start the read of the given event revision. 33:14 So where do we start reading events from on the given stream and a max 33:18 count to configure how many we wanna pull back if we wanna limit. 33:22 From here, I'm gonna open up a try catch block 33:27 where we catch a error. 33:29 And now what we're gonna do is, again, we can use await in here 33:33 because again, this is still an async method. 33:37 So we're going to iterate asynchronously over the iterator 33:41 using for await. We're allowed to use for await syntax 33:45 on the return type. The return type from read stream 33:49 is of type async iterable iterator. 33:51 So this is a generic 33:53 async generator return type. So with this, we can await 33:57 directly. So we're going to await result 34:01 of iterator. So per emission of this 34:05 async generator, we are going to iterate over the event 34:09 stream. And now what we're gonna do is check to see if 34:13 resolved. 34:15 Again, this is after we have asynchronously 34:18 resolved value. 34:19 The event from the stream that CurrentDB hands back to us is now 34:23 resolved, 34:25 and we can access the underlying properties here. 34:27 The commit position, for example, 34:30 tell us where it is in the stream, and then the event itself. 34:33 And we're going to check on the event a property called 34:37 isJSON, which will confirm that this is a JSON 34:41 object, which it should be because remember, 34:43 when we are writing to the 34:45 stream, we convert everything to the JSON event 34:47 type. 34:48 So we just wanna have some type safety here and make sure this is the correct 34:51 format. If it is, we're then ready to return 34:55 this resolved event back to the caller through an async 34:59 generator emission. To emit, or in this case, 35:02 yield from the method, we simply use the yield keyword, 35:06 and we can choose the value that the caller will get back here, the 35:09 resolved.event. 35:12 And what this now means is callers can call this 35:15 read method using for await syntax. 35:19 And each time in that caller's loop, we'll be going through 35:23 awaiting another value from the stream and yielding the latest one. 35:27 And this is the power of the async generator function. 35:30 We can have a controlled flow of processing these events using 35:34 async await from the caller side. The last thing I wanna do is some error 35:37 handling. So let's check to see if the error is instance of stream 35:41 not found error, meaning it was an invalid stream that, 35:45 that never existed. For example, an incorrect account ID, 35:49 which is what we're gonna use to identify the stream. 35:52 We'll just return because there's nothing to read. 35:55 Otherwise, let's re-throw the error. 36:00 And now at this point, we have our event service append 36:04 and read fully set up. This is amazing. 36:06 It now means we can actually read and interact with our 36:09 CurrentDB event store, 36:12 which is awesome. Next up, we're finally ready to start 36:15 using this event store service where we'll create our accounts 36:19 domain code, where we'll add a controller to manage our 36:23 account functionality so we can add operations onto 36:27 accounts. And I'm gonna show you a basic 36:29 domain-driven design 36:30 approach that will allow us to write domain-oriented code 36:34 with events that will best utilize our event-driven architecture 36:38 backed by EventDB store. So let's go ahead and start doing this 36:42 next. All right, so next up inside of source directory, 36:45 let's 36:45 create a new folder for the account domain. 36:49 And the first thing we wanna do is create the 36:53 account.ts. And here we're gonna define the events 36:57 that 36:57 can occur on an account, which will of course be persisted to the 37:00 database 37:01 ultimately, but also are in a shape that our application code can 37:05 use to actually apply our business operations, 37:09 like updating the balance of an account. 37:12 So the first thing I wanna do is export type AccountEvent. 37:16 This is going to be the underlying event that stores in the database, 37:21 so we know this needs a type to identify it. 37:24 I'll have one for AccountOpened, 37:28 and then the data property for the underlying data that g-- that will get 37:32 stored. So I wanna store the account ID 37:36 and the owner as a string. 37:40 So we'll mark this as one possible type and then fill in the next 37:43 one. The next one will be MoneyDeposited 37:47 when we 37:47 add money to the account. The data for this one will include the 37:51 amount, 37:51 so how much money are we actually putting in. 37:54 And then finally, we will allow the type for 37:57 MoneyWithdrawn. So again, these are all the business 38:01 domain events that can occur on our given account, so this 38:05 would change depending on your domain and the operations inside of it. 38:10 Again, we'll control the amount for withdrawals, 38:13 and let's go ahead and 38:13 export interface AccountState. So this 38:17 AccountState is how we're gonna actually interact with an account 38:22 in our application. 38:24 This will have the exists property, which control if it actually does 38:28 exist, the underlying optional owner, 38:32 and the balance. So these are the live properties we should be 38:35 working with with a 38:35 given account. 38:38 Next, let's export const initial state, 38:41 which will initialize to the AccountState type. 38:45 So here we'll set exists to false and balance to zero. 38:48 So this is essentially 38:50 an account that is not 38:53 actually initialized and set up yet, 38:56 which is gonna be the first initial state that we're working with. 38:59 And then we're gonna create a function here called evolve, which is almost like 39:03 a reducer function that will take in the AccountState, 39:07 so a given AccountState, and then a new event. 39:11 So if you ever use React Redux, this is gonna be a very similar pattern h-- to 39:15 there, where we'll take in the existing state 39:18 and then the new 39:18 event that we want to apply to that given state, and then return 39:22 back the new state. So following a very functional-driven 39:26 approach where we will now 39:28 switch on the 39:31 event.type. And for each event type, we want to 39:35 handle this differently based on our domain logic. 39:38 So this is where the domain logic really sits in the application. 39:41 For example, we can have a case for AccountOpened, and 39:45 here it will return the new AccountState where exists should now be 39:49 true. 39:50 The owner will be the event.data.owner. 39:55 And again, the starting balance should be zero in 39:57 this case. 39:58 Next up, we will define the MoneyDeposited. 40:02 So here all we're gonna do is return all of the existing 40:06 state. Nothing else should change, but now we wanna update the 40:10 balance. The balance should be the starting balance 40:13 plus the new balance from the incoming event. 40:17 So that'll be the amount for MoneyDeposited. 40:21 So adding on the deposit to the existing balance will give us the new 40:25 balance for the account. And again, this is the beauty 40:29 of the domain event-driven approach, where we're always 40:32 calculating the current state at any given time based 40:36 on the state that was passed to us. 40:40 And this will allow us to do just that. 40:43 We'll then add a case for MoneyWithdrawn. 40:49 Return 40:51 state. 40:54 And now the opposite, the balance will be state.balance minus 40:58 the event data.amount. 41:00 So now we have a function to actually apply our events to a 41:04 given AccountState. We're ready to head back to the account 41:08 service 41:08 and actually use this in our event store. 41:11 So super excited to try this out. All right, so now let's go ahead and 41:15 create the new accounts.service.ts. 41:20 This will be an injectable NestJS service. 41:24 Export class AccountsService. 41:28 And in here, we're now going to use our 41:30 constructor-based injection to 41:32 inject the existing event store, 41:36 which will be of type EventStoreService. 41:39 So now we have access to the underlying event store. 41:42 We're gonna implement the important method in here, async load, 41:46 which takes in a given account ID 41:49 and an optional to revision as a BigInt. 41:53 And now what we're gonna do is load an account ID from our event 41:57 stream. The first thing I wanna do is actually create a helper 42:01 function up top called stream that takes in 42:05 an account ID and returns back the common 42:09 stream name. In this case, we'll just make this a template literal 42:12 with the 42:13 account prefix and then output the account ID itself. 42:16 So this will help us identify the stream if we're looking in the web UI 42:20 later. Now inside of load, I want to set 42:24 the state as a let, the initial state here to 42:28 initial state from account.js. I then want to create 42:32 let revision 42:34 set to append StreamState type 42:39 and set it to the No Stream constant. 42:43 So this is essentially a way to pass into event 42:47 store 42:48 To current DB, 42:50 if we pass in this no stream constant, then we want to create a new stream if 42:54 it doesn't exist. 42:56 And so now what we're gonna do is use our for await 43:00 syntax to await const E of 43:03 this.eventStore.read. And 43:07 what we can now do is pass through our stream wrapper to turn 43:11 this into a 43:11 stream name, pass through the account ID, and now we're invoking 43:15 the async generator function inside the event store. 43:19 And in here, we'll now get access to each underlying 43:22 event on the 43:23 underlying stream from the database, which is excellent. 43:27 So now for each event, we're gonna handle this properly. 43:30 The first thing we're gonna check if, if to revision that got passed into 43:34 us does not equal undefined and 43:38 event.revision, again, this passes into us from current 43:42 DB. They give us the revision for each event. 43:45 If it's greater than to revision, this means that we 43:49 are caught up on the stream, and we don't need to go any further. 43:52 We are at the to revision that the user has asked us for, so we can 43:56 break out of here. However, if we're not at the correct 44:00 record on the stream, then we know we need to handle this 44:04 appropriately. So now with the magic of event-driven 44:07 design, what we 44:08 need to do is evolve the initial state here 44:11 to the current state inside of the event stream. 44:15 Well, to do this, we simply reset our local state variable 44:19 to 44:19 the return value of evolve function. 44:23 The evolve function takes in the current state, which will be initialized 44:27 to the initial state, returns us back a new object, 44:31 so there's no 44:31 danger of mutating this. And then we pass through the event we want 44:35 to 44:35 apply to the given state. So here that is the type 44:39 event.type from the event, which is the identifier 44:43 for the type of event, 44:44 which of course will correspond to the 44:48 event type that we set up in our account file. 44:52 So we have the type. Now we also wanna pass through the data, 44:56 which will be event.data, which is the actual JSON 45:00 data got stored in the database for this particular event. 45:03 Now we'll have to use this cast as account event here to make sure this 45:07 type is handled properly. Now at this point, our state 45:11 has the event from the stream applied to it. 45:13 The last thing we need to do is set the current revision to 45:17 the event.revision that we are currently on. 45:20 So we exit when we are at the correct record on the stream. 45:24 And now we will go through every single event on the stream, 45:27 reapply it to our local state, and we will be caught up and 45:32 have the current up-to-date version of the account with the latest 45:36 balance applied to it after all of this work, 45:39 which is exactly what we want. After the loop, we'll check that 45:43 state.exists to make sure that we 45:47 actually have an account that's opened at this point. 45:51 If we don't, we'll throw a new not found exception 45:55 because 45:55 this means this account by this ID does not exist. 45:59 It has not been opened before. 46:02 So we'll say account with ID has not been found. 46:05 And again, this is business logic where we're saying 46:08 if the 46:09 exists property has not been set to true, we do not want the account 46:13 to be loaded into the system. So this is a business constraint we're applying 46:17 here by making sure the account of the opened event has been 46:20 found on the given stream, which is exactly what we want to 46:24 do. So again, we have this business logic being applied 46:28 here in a nice, 46:28 clean way, and this is always going to be 46:31 occurring whenever we access this account in the system. 46:36 Otherwise, at this point, 46:38 we're gonna return back the underlying state and then the revision 46:41 itself. 46:43 So now we have the ability to load new records, new accounts from the 46:47 event stream, which is awesome. Now, I also want to create a 46:51 private async append method 46:54 that can take in the account ID, the new account event, 46:59 the expected state, so append stream state, 47:03 and finally, what we're gonna call the actor ID, 47:06 which will be the 47:07 underlying user ID that wants to add a new event in our 47:10 system. 47:11 So in append, we can use const result, set 47:15 this to eventStore.append, 47:18 which expects all these parameters. 47:20 We need to pass through the stream identifier, 47:22 which will be the stream wrapper 47:24 function with our account ID. 47:26 Then we need to pass through the underlying events themself. 47:30 So for this, we'll just pass through a new array where 47:34 we spread an event object, which will be all of the 47:38 current event properties that are being passed in as the account 47:41 event. So that includes the account type, which we rely on, 47:46 and the account data. But we also wanna then override the 47:49 metadata on the given event here so that we can set our own 47:53 custom actor ID property, which will be the user ID 47:56 associated with this event. 47:59 Finally, don't forget we need to pass through the 48:01 expected state, which does our 48:03 optimistic locking check to make sure we're caught up with the event stream 48:07 before 48:07 persisting it. We'll then return back the 48:11 revision as a number 48:13 result.nextExpectedRevision, 48:17 which is gonna be what the caller can use as the expected 48:20 revision in the next call. So it's the latest version of the stream 48:24 here. 48:25 And so now at this point, we have the ability to actually implement 48:28 our domain 48:28 methods. So I'm gonna create an async 48:32 open method. 48:34 This will take in the 48:36 owner of the account, 48:38 the actor ID. And now what we're gonna do 48:41 is to create the 48:42 account ID. I'm just gonna 48:45 use random UUID. 48:47 Use V seven here from bun. 48:50 And then we're gonna call await this.append, pass through the 48:54 account ID. 48:57 And then the underlying event we're trying to add. 48:59 So in this case, now it'll be an account open type 49:04 with the data 49:05 that includes the account ID and the owner. 49:09 For the expected state, we can use the 49:12 no stream constant, 49:14 which indicates this doesn't actually exist yet, so nothing will fail when 49:18 it passes this through to current DB as the expected stream. 49:23 And finally, the actor ID itself, which is who is gonna 49:27 be added to the event metadata. 49:29 We can return back the account ID if this is successful. 49:33 So now we have the ability to open an account. 49:36 Let's add async deposit. 49:39 This takes in the 49:41 ID, 49:42 amount, and actor ID yet again. And now the 49:46 first thing that we need to do is get access to the current revision on 49:50 the event stream that we actually want to update. 49:53 So for this, we'll destructure revision. 49:56 Remember from our new load method that takes in the account ID. 50:01 This will give us back the current event stream with the latest revision, 50:05 which we're now gonna use in the append method. 50:08 So append will now take in the underlying ID of the account. 50:12 We'll add a money deposited type, and then we'll add 50:16 the data object 50:18 with the underlying amount we're adding to the account, 50:22 the revision, 50:23 and the actor ID. If this succeeds, again, return 50:27 back. So now we have the ability to deposit 50:30 money into the 50:31 account. And again, this is all done by updating the stream with 50:35 the 50:35 latest money deposited event, which includes the raw amount. 50:39 We're not persisting the balance at any time. 50:42 That's the key point to understand here is that we're simply 50:46 just storing the events and only when we load these events 50:50 back from the event store DB do we then 50:53 apply all of them to get the current balance, which is the 50:57 beauty of this event store approach. 51:00 We have the flexibility to 51:03 see the actual state of the account at any point in time from the event 51:07 history. Really a different way of thinking about 51:10 your 51:10 application's data model. So finally, let's add one 51:14 more method to async 51:17 withdraw. This will be of type string, 51:20 amount number, 51:22 and actor ID string. 51:25 We're gonna use the same exact syntax here. 51:28 So now we're gonna actually get the underlying current 51:32 state and revision from the account using our 51:37 load method. Make sure this is spelled correctly. 51:40 So we have both the current state and revision based on the events applied 51:44 to the 51:44 stream up to this point. 51:46 What we're gonna do is some business logic check here as well 51:50 and check if 51:50 the state.balance is less than the 51:54 amount that we're asking to withdraw from the account. 51:58 We know this is a violation of our business logic. 52:01 We cannot subtract past the balance. 52:03 That will keep our account in an inconsistent state. 52:07 So now we're gonna throw an unprocessable entity exception 52:10 and say insufficient 52:13 funds. Otherwise, we can 52:16 append to the event stream by the account ID 52:21 a new type of money withdrawn 52:24 and set the data 52:26 to be the underlying amount 52:29 passed through the current revision and the actor ID. 52:33 So now at this point, our account service is fully complete 52:36 and ready 52:36 to offer the open deposit and withdraw through our 52:40 event-driven approach using the event stream, which is excellent. 52:45 Now, the last thing I wanna build here is an async history 52:48 method, which gives us this audit log 52:51 functionality where we can 52:52 actually see all the events applied to the given stream. 52:57 So for this, I'm gonna create a entries array. 53:01 And again, we're gonna iterate over our event stream 53:03 by using const 53:04 E of this event store.read, pass through 53:08 the stream with our account ID. 53:11 And now we're simply going to push on the entries 53:15 array 53:16 a new object which will include the revision of 53:20 the event that we're iterating over, 53:23 the type of the event, the underlying data, 53:27 metadata, 53:29 and finally at. So the event gives us the at 53:33 created property, which is when it was created in the system. 53:37 Very useful. And so you can see how easy this audit log 53:40 functionality 53:41 is to get access to. Thanks to the event stream, 53:44 we have a written 53:45 history of the account at any given point, which is really 53:49 nice. Make sure we return back that 53:52 entries array as well. 53:54 And now at this point, we are ready to actually add our 53:58 accounts controller where we'll expose this 54:01 functionality over HTTP. So add 54:05 a controller decorator 54:08 called accounts, 54:10 export class accounts controller. 54:14 And in here, we're gonna now inject the accounts 54:18 service. 54:20 I'll call it accounts as a member variable. 54:23 I also wanna define a couple of Zod schemas. 54:26 So we'll add the open schema to open a new account. 54:30 In Nest twelve, we can use Zod as our request validation 54:34 library directly by pointing these Zod schemas to it, which is 54:38 very nice. I'll define a Zod object where we 54:42 define the owner name as a string 54:46 and say the minimum is one to enforce a min length on the 54:50 request. I'll also have the amount schema, 54:54 which will be a shared schema for when we want to affect the 54:58 amount on a withdrawal or deposit. 55:01 This should be a number 55:03 .int and positive. So now with the schemas in place, 55:07 let's add our post route where we'll add the open 55:11 method. 55:12 We'll use the @body decorator and pass through the new 55:16 schema property, which takes in the Zod schema directly, 55:20 and the validation will be applied automatically. 55:24 So the DTO type now can use Zod.infer 55:28 type of open schema. So that's the really nice bit here 55:31 is we get 55:32 both the runtime and build time type safety. 55:35 Now for our actor ID, I'll just 55:39 extract the incoming HTTP headers with the headers decorator, 55:43 and let's accept this actor ID header. 55:46 In reality, this will be set to your JWT user ID, 55:51 for example, 55:52 to identify the given user. In our case, we can 55:56 default this to anonymous and just take it in from the HTTP header 56:00 itself. So make sure you've also imported headers 56:04 from 56:05 Nest common. And now all we have to do is return 56:09 this.accounts.open, 56:13 pass through the dto.owner and the actor. 56:16 And now this functionality is exposed on our post route, which 56:20 is awesome. 56:21 Next up, I'll add another post route for 56:25 the deposit. So for this, we'll accept a route 56:29 parameter with :id. We can call this 56:32 whatever. We'll destructure this from the route 56:35 parameter 56:36 later on. 56:38 Call this deposit. So that'll be the account ID we're 56:40 depositing into as part of 56:41 the route URL. Call this deposit. Now to extract 56:45 that route parameter, we use the @param header decorator, 56:50 where we can extract it by its name ID, type 56:53 string. We'll extract the HTTP request body again. 56:58 Now the schema will be set to the amount schema, which will be 57:02 dto type z.infer type of 57:05 amount schema. 57:07 And finally, let's use that same actor ID header to extract 57:11 the incoming user. 57:14 Now I'll just return this accounts.deposit and pass through all of this 57:18 data to the account ID, 57:21 dto amount, and actor. Now we have deposit. 57:25 Now for withdrawal, I can just copy and paste the deposit method 57:29 and just swap out the name to 57:32 withdraw. Call this withdraw method, but everything else stays 57:36 the same. Now we just change the method name to 57:38 withdraw. 57:39 Now we have withdraw functionality. 57:42 Next up, let's add @get 57:45 and the route parameter will be the account ID we want to retrieve. 57:49 So this get is going to be async get 57:53 pass through the route parameter. 57:56 Again, ID type string. 58:00 And the next thing I want to do is accept a query parameter as well. 58:04 So we'll use the @query parameter, 58:07 and this will be called revision. Again, this will allow us to choose 58:11 which event we want to build up to the current account. 58:15 So essentially letting the client choose 58:18 where on the account stream that they want to return the account from, 58:22 which is very nice functionality of the event-driven approach here. 58:26 Get that built-in history for free. 58:30 So this will be an optional revision type string. 58:34 And so now let's get access to the current account state using 58:37 the familiar accounts.load method that passes through the 58:41 ID. 58:43 We'll check to see if revision does not equal undefined, 58:46 meaning the 58:47 user did pass it. We need to turn it into a big int, so pass 58:51 through the revision string to big int constructor. 58:55 Otherwise, we continue to pass through undefined here 58:58 through a ternary 58:59 expression. So now we have the current account state. 59:02 We just simply return that back to the caller. 59:06 Finally, I want to add one more route 59:09 that includes the account ID called history. 59:13 This will be history method where we'll extract 59:17 the route parameter 59:19 ID, the account ID. 59:21 This will return us back the full account history. 59:23 So this.accounts.history 59:26 and pass through the ID. So we can see all of the events 59:30 applied to that given account stream. 59:33 And now we have the accounts controller fully wired up. 59:36 Let's go ahead and now make sure we also have the account 59:40 module. 59:41 So @module decorator, 59:44 export class accounts module. 59:49 And let's choose the providers with the account service. 59:52 So we wire that in. 59:54 And importantly, the HTTP controller now, the account controller. 59:59 And make sure at the root app module, we now pass through the 1:00:03 account module. 1:00:06 Now our application is still running here. 1:00:08 You can see all of the new account methods have been registered, which is 1:00:12 excellent. All right, so now to actually test this functionality 1:00:15 out, 1:00:16 I've created this new demo script at the root of the project 1:00:20 that you can get access to in the repo and copy over. 1:00:24 This is simply launching a series of fetch requests against our new 1:00:28 API and setting it up in a nice demo state. 1:00:32 I've also added a new demo script to the package.json to 1:00:36 use Bun to execute this script. So let's go ahead and try it out. 1:00:39 You can use Postman directly if you prefer to invoke our methods. 1:00:44 I have Bun running the dev server in one terminal. 1:00:47 And now in this other terminal, let's run Bun demo. 1:00:51 This is going to go ahead and launch the request at our database, 1:00:54 and we can walk 1:00:55 through what's actually happening here. 1:00:58 So to start, we're first opening up an account by 1:01:01 launching a post 1:01:02 request at our accounts endpoint with the account owner. 1:01:06 And you can see this comes back as a 201 with the newly created account 1:01:10 ID 1:01:12 Next up, we are launching deposits and withdrawals 1:01:16 using our deposit and withdrawal endpoints. 1:01:19 Here, all we're doing is passing the amount 1:01:22 that we want to apply 1:01:23 to the account deposit or withdrawal, not the current balance. 1:01:27 And so here we deposit 100, subtract 30, 1:01:32 and add 50. These all come back as 201s. 1:01:35 And then look what happens next. We actually try to send a 1:01:39 withdrawal for an amount that exceeds our account balance. 1:01:42 Here we have our domain logic actually applying our business 1:01:46 rule, so we get that 422 back in processable 1:01:50 entity, 1:01:51 exactly what we want because we have insufficient funds. 1:01:54 So again, always maintaining the correct domain 1:01:56 state at any given point, which is 1:01:58 very nice. Next up, we are getting the account by its 1:02:02 ID to get the current state of the account, which includes the 1:02:05 latest balance by applying all of the previous events on the 1:02:09 stream to the account, so this still gives us the latest live 1:02:13 balance and all the other details on the account. So very nice. 1:02:17 But what's really nice about this event-driven approach now 1:02:20 is we have the time 1:02:21 travel functionality, where we can now replay up to a given 1:02:25 revision. 1:02:26 So here we're calling the same endpoint, but now passing through that 1:02:30 revision query parameter. So by setting this to zero, 1:02:34 we're asking it to replay us the state of the account 1:02:39 when the revision or 1:02:41 event stream was at zero. Well, that will be at the initial 1:02:45 state where balance was zero. As we keep incrementing this, 1:02:49 well, revision one corresponds to when we added 1:02:53 that $100. So the balance of the account at 1:02:56 that point is 100, and so on for the next when we withdrew. 1:03:01 So this amazing functionality gives us the ability to replay the 1:03:05 live history of the account at any given point 1:03:09 by applying the events on the stream up to that revision, which is the 1:03:12 power of this Event Store DB. 1:03:16 Finally, you can see here the audit log gives us 1:03:18 the full event 1:03:20 history on the stream, so we can see every single event type 1:03:25 and the underlying data for that event. 1:03:27 And so you can see how building an audit UI using Event Store DB 1:03:31 is so easy because we already have all of these events 1:03:35 stored naturally on each 1:03:38 entity, in this case, the account. 1:03:40 So we have this full history around every single entity, which makes 1:03:44 this application architecture so suitable for these kinds of 1:03:48 entities. Finally, you can see at the bottom here we have 1:03:52 this message 1:03:52 about browsing the stream at localhost 2113. 1:03:56 So go ahead and open this up inside of your browser. 1:03:59 All right, so now at localhost 2113, we're actually looking at 1:04:03 CurrentDB's UI. Now this is available because we're 1:04:06 running in Docker Compose. And remember, we have this port forwarding in place, 1:04:10 so we can access this UI 1:04:13 at 2113. And this is how CurrentDB allows us to 1:04:17 interact with our saved data. We can get access to the underlying 1:04:21 resource usage of the DB as well, which is very nice, 1:04:25 see the status of the Event DB cluster 1:04:29 and node status itself. In our case, we want to navigate to the legacy 1:04:33 UI and go to Stream Browser, where this allows us 1:04:37 to actually look at the real underlying streams that are stored in the 1:04:41 database. So now we can look for the account stream 1:04:44 that we've 1:04:45 created on this demo example, 1:04:47 which is the latest stream here. We can click into this and now see the raw 1:04:51 events that make up this stream inside of the saved database. 1:04:56 So you can see the account type, the creation date, and even the 1:04:59 underlying JSON data that includes each event 1:05:03 and the metadata to see who actually created it. 1:05:07 So this makes debugging our Event Store DB very useful and easy. 1:05:11 So I hope you learned so much in this lecture about Event Store DB, 1:05:15 CurrentDB, and this whole architecture where we have 1:05:19 the 1:05:19 ability to rebuild the state of our entities at any given 1:05:22 revision, which is the true power of the Event DB 1:05:25 architecture. 1:05:26 Thank you so much for watching, and I'll see you in the next one.