How To Implement Domain-Driven Design (DDD) in Go

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
foreign [Music] Percy today we will be talking about one topic that is really cool and really useful once you get to know it and that's domain driven design uh specifically domain driven design in go and how to implement it efficiently I released an article about this about a year ago and that's one of my most loved articles so I'm super thrilled to be making this video right now and let's get going so microservices has become this very popular approach to build software in the recent years now microservices are used because they're scalable and flexible software it's very easy building a small piece of software that runs and performs one single task and make that really good compared to making this huge monolith and it also allows you to replace these small pieces very easily so it's very favorable once you start using it in this article we will be building a Tavern an online Tavern and while we are building the tavern we will explore certain parts of domain-driven design and hopefully it will make it a little bit easier to understand domain driven design when we implemented one single piece at a time because whenever I myself started reading about domain driven design my head kind of exploded there were so many terms so many things to learn and I just was overwhelmed and if you don't know um why my head exploded when I started reading about domain driven design let's have a look at Eric Evans who is the inventor of domain driven design his book contains a graph of all the items in domain driven design as we can see here there's a bunch of terms to learn and everything's connected to each other and it's kind of hard to wrap your head around the first time and there's a reason why Eric Evans needed a book on 500 pages to explain everything so first off I'd like to point out that this article describes my interpretation of domain-driven design and how to best implement it in go and it's based on my experience working with go for many many years and how I've noticed that what works best for me and my team in this video I will be naming the folders inside the project corresponding to The Domain driven design term so if we have something called entities for example they will be in a package called entities this is not something I recommend you to do in a production repository in a real repository this is just to Showcase and make it easier to understand what's what I have a second video an article where we restructure a little bit so in this video we will learn the terms we will learn to implement it and in the second video we will structure it a little bit making a little bit more clean [Music] um I see many heated discussions on the internet about DDD um many people are discussing how to implement it it's a it's open for interpretation so many people are doing it in different ways um one thing that I kind of get sad about is that people are often discussing small typical terms like this term means this and this and I think it's not important I think the important part is follow the methodology that Eric is trying to you may make a point about and DDD is a huge area we will look at how to implement it but before we do that I'm gonna make a small recap about what domain driven design is so domain driven design is a way of structuring and modeling our software after the domain where it belongs to what this means is that a domain first has to be considered before that software is written and the domain is the topic that the problem or software intends to be working on so DDD kind of Advocates that the engineering team should meet up with the subject matter experts the smes who are experts within the domain so that the subject matter experts can share their knowledge with the engineers so for instance if I were to build a stock trading platform uh would I as an engineer know best how to build one well programmatically I would be the right person but I wouldn't have any trading knowledge I wouldn't build a much better platform if I had a few sessions with Warren Buffett who could explain the domain to me and the architecture in the code should reflect this domain so we will see this whenever we are writing our own Tavern so let's go ahead and make a quick story a story is used the story is used to better make us understand the topic at hand so this is the story about Dante a gopher who wants to build an online Tower Dante knows how to write code but he doesn't know anything about running a tablet which is problematic so one day when he's at work walking he meets a guy in a top hat and the top hat approaches him saying do you need help with your Tavern Dante is thrilled to have a talk where they discuss how to run a Tavern Dante asks questions such as how do we handle regular drinkers and the top had kind of nicely replies well let's call them customers not drinkers and he goes on explaining how a Tavern operates we need customers we need employees we need banking we need suppliers so why are we talking about Dante in the top hat well we're talking about them because we can use them to explain what's going on in domain-driven design terms so what has happened here is don't be in the top hat had a meeting they had something called a domain modeling session it's a session where the subject matter expert the top hat explains to don t the engineer about the domain he explains things that are needed for a tablet and this is done to learn something that we call the model and the model is an abstraction of the needed components to handle the domain the banking the customers the billing for instance in domain driven design we often talk about the root or the core domain in our case the core domain will be the tavern and we have something called customers which will be a sub domain and the top hat also points us to us that we don't call the customers Drinkers and what has happened here is we have something called ubiticus language now basically it's the basically it's about talking the same language between the experts and the engineers I've been to many firms in my life and I've seen many places where different departments are naming the same thing differently and they're referring to it in a different way which sometimes becomes problematic now we need to find a unified language which everyone in the team were involved understands this is called that's called the ubiticus language so I think that covers very quickly about domain-driven design you have the domain you have basically the topic the core topic and then that's divided into sub topics or sub domains so um I think basically we are ready to start coding so one of the first first things that we will begin coding is entities and value objects now entities are uniquely identifiable items or structures and they are mutable so we have for instance a gopher or a person which is uniquely identified by their social security number for instance but we can also modify them they can change and they can put on a top hat value objects are non-identifiable items which are immutable we cannot change them so let's go ahead and start implementing a little bit inside of our code so let's go ahead and start implementing the entities and the value objects and learn a little bit more about so it's time to start building the tavern let's begin by making a new folder which I've created and let's start by initializing a go module so go mod in it in my case I will do github.com firstly polymer DDD code so once that's created we will Begin by creating a domain folder the domain folder will be storing all the subdomains that we need but let's begin by The Click The Domain so we're creating the domain folder so before we can implement the domain we also need the entity so let's create a second folder called entity yeah and as I said the entities are structures that are has a identifier a unique identifier and that can change States and by changing State we mean that the values can update they're mutable we'll Begin by creating two entities persons and items and I like keeping my entities separate from my domains because that allows us to reuse the entities in different domains so let's go ahead and create two files in the entities we have item.go and we have person.go so inside person we will Begin by creating the person package so that's where the entities package so this holds all the entities that are shared across sub domains and let's call it entity and we're going to do a new structure which is person and a person is an entity that represents a person in all domains so let's go ahead and create a unique identifier for the presence so the ID is the identifier of that entity so I'm gonna go ahead and use the Google uuid which is a package which is really nice for [Music] um selected the wrong one so let's change that to Google it's really nice to create unique identifiers I do recommend it so let's import that and do go mod tidy it will fetch it for us and let's go ahead so a person can also have a name so let's add a name and we can have the age as an integer so this is our person entity it's something that we can represent as an entity so let's go ahead and do the same thing for and items so I'm just going to go ahead and copy whatever we have I'm gonna change this to item some item and an item has a unique identifier because it's an enter item because it's an entity we do have to have an identifier we have a name of the item and maybe we have a description instead of an age which is also a string okay so great we have defined two entities and it's as simple as that we have uniquely identifiable and we are having the fields as a capital letters which in go means that we can access them from outside of the package and we can modify it there can be occurrences where we have structures that are immutable and does not have unique identifiers and they these structures are called value objects as we said before so let's go ahead and create a new folder folder called value object and let's create a file called transaction so a transaction is something that's immutable we cannot change it the transaction has been performed um it's there we cannot do anything about it once it's done in a real world application a transaction would of course have an ID connected to it but it's not mutable and that's the kicker here so let's create the package called value object and let's create the transaction structure and transaction is a value object because it has no identifier and is unmutable so let's go ahead and this time I'm going to use lowercase values so no no other domain will reach in and change the values so let's go ahead let's use uuid again from Google so we will store the account from or the person from and we'll store the person ID who we transfer the money to which is great in a transaction to know where from and to and let's also have created at which is the time when the transaction occurred so we have entities we have value objects I hope that makes sense the next thing to look at is the aggregate components we cannot explain every real life thing with entities and value objects sometimes you need to combine them that's what an aggregate is so an aggregate is a combination of entities and value objects when you combine them you get an aggregate so in my example we will have a customer for instance and the customer has a something called a root identity now the root identity is used to identify the Aggregates this is why the entity has to have a uniquely identifiable identifier so whenever we create a customer we can say that the a customer is a person and they are identifiable by the person ID so the root entity you can have multiple entities we can have products which is uh um which is items in an array but we don't connect the product ID to the customer ID that wouldn't make any sense so it's not the root entity it's a sub entity and a customer can also have transactions so it's really important that you maintain only having one root entity so you can identify the aggregate based on that and a reason for Aggregates is that the business Logic for say customers should be inside the aggregate not inside the entity entities are dumb they're just placeholders for information now the aggregate can contain business logic um so let's go ahead and do the same thing again let's open a new folder and let's call it aggregate and inside aggregate will create our first aggregate which is a customer and in here we have the customer so the package aggregate holds or Aggregates that combines many entities into a full object let's say a full object Edge aggregate like that so let's begin by creating the customer struct and the customer struct is an aggregate so it combines the entities I know I'm repeating myself but it's to make you understand this so person is the root entity of the customer which means that person.id is the main identifier for the customer so let's make entity.person and we can also hold products so a person can buy many products and a customer can also perform transactions so let's add transaction now you might notice that I set all the fields to lowercase which means they're not accessible from other domain from from outside I made this decision after talking a little bit to Middle smoker at three dot Labs we had a discussion about this and Aggregates should not be accessible directly to grab the data and the data is not accessible from outside so they're going to be lowercase and also notice how I'm not using any Json tags or anything and we'll cover that later but it's not up to the aggregate to decide how the data is supposed to be formatted we look into this soon also we set all the entities to pointers because they can change State and we want this to reflect across the whole runtime when everything something changes yeah so if you have a person in multiple places The Exchange should reflect here [Music] um and of course the transaction is not a pointer because it cannot change there's no reason to have it as a pointer so great we have a aggregate in place so up until this point we have only created entities value objects and Aggregates we've only created a bunch of structs um well it's time to start implementing some actual business logic and we will be starting with something called factories now the factory pattern is a design pattern that is used to encapsulate complex logic inside functions for creating The Wanted instances without the caller knowing anything about the actual implementation details so Factory pattern is very common you see even outside domain driven design it's used widely so I want to show you a great example of the factory pattern and one example is the elasticsearch package on forgo and if you can see here this is a factory for creating a new client so they have a new client function which accepts a configuration and returns a pointer trunk lines so if you see what's going on here and they are calling the new transport function so if you go there you can see there's a lot happening in here which we don't want the developers who are using the library to really know anything about they don't have the domain knowledge for what needs to be done when creating a client they shouldn't have to the factory should handle that for us and that's what's happening here so whenever we create a new elasticsearch client a bunch of code is being executed and fixing everything for us we don't have to care so domain driven design suggests factories for creating complex Aggregates repositories or Services which we will look into soon so let's go ahead and create a factory function for customers now customer is still very simple class but we should really suggest we should really just try to make it as easy as possible so we have a new customer and when we create a new customer we will accept the name we will return a customer and an error so something can go wrong when we create new customers and we want to handle that for the users so new customer is a factory to create a new customer aggregate it will validate that the name is not empty for instance so if name equals empty we will return customer and let's always return exported costume error which makes testing a lot easier so let's return error error invalid person and let's go ahead and go to the top and create that error so it will be error invalid person equals a new error a customer has to have a valid name for instance so we have a valid name let's create a person entity because we need that we need a root entity so we will assign the name we have the name let's assign that we will generate a new uuid by using the new function let's import the library so now that we have the person entity we can return the customer we can assign the person as the root entity we can generate an empty array of items or products and we can also generate an empty array of transactions just to make sure that just to make sure that we we don't hit the users of our aggregate with nil pointer exceptions for instance now everything is initialized the arrays are initialized they won't get nail pointer exceptions whenever they try to reach the products um the factory has handled that for us so this is a simple Factory um really not much to it just a helper basically to generate your structure um and as always whenever we have whenever we have business logic like this in place we should always test it and I'm just going to go ahead and create a aggregate test package real quick [Music] um just to test our Factory so let's go ahead and call it test customer new customer we are importing the testing library and let's build a test case and the test case will have a name and we will have an input name to the to the factory X in fact we will have an expected error returned from the function and let's create some test cases so I'm doing some table driven tests here so let's create a test case so one test case is of course when we have the empty name we want to know that the error is returned empty name validation and we have the name and we expect an error which is aggregate because as you see here we can reach the error since we export that because it's important whenever we return an error that is custom we have to allow it to be exported so that the users of the package can validate and check that or what's going on so that's one test case the second test case is of course a valid name let's always test the happy path as well so let's add a customer with Percy bomber as name and we expect nil to be returned we don't expect an error to be returned um so let's go ahead and also Loop through those test cases so range test cases we will run the test we will assign the test name and we will see the test was test not name so we will run each test case we will execute a unit test for each one and we will do aggregate new customer and let's pick the name that we set and we will check that the errors is error and the expected error so if the error isn't the same as we expected we of course want to return an error expected error from but we got let's do that in case it's new um so the V the V here stands for like interface so even if the error is nil we won't get a nil pointer exception so we have a test we can run the test to make sure that we are getting the correct input output and we are as you see here we ran the test cases and they succeeded which is great we know that our Factory is working so I hope the factory makes sense the factory starts a little bit on the business logic it creates our instances and helps us with whatever has to be performed and whenever I come from another domain and has have to create a customer I don't have to care about what happens when I create a customer I have to care about the input values and basically the output error that's what I need to care about we can create a bunch of stuff that's what we have right now we have entities we have the aggregate which combines them into um b a customer but before we talk a little bit about the customer not having any data tags no Json tag no B's on tag no CSV and this is because Aggregates are stored by repositories foreign aggregate is a combination of entities or value objects but when we store them or when we manage them we're using a repository so the repository is used to store and manage aggregates repository pattern is again it's a pattern which you not only will find in domain driven design it's a pattern widely used in many many many other paradigms now it's one of the patterns that I love the most I love repository pattern once you learn it you will probably use it all the time or at least it was like that for me now the pattern relies on hiding the implementation details behind a interface and this is allows what what repository pattern allows us to do is allows us to build very modular and changeable day um changeable software so basically we can have a in-memory repository which stores customers in memory whenever we do unit tests but then we can also have a MySQL Repository and whenever the managers comes and say hey we're changing from my sequel to mongodb for instance we can build a new repository for mongodb and fulfill the same interface as the MySQL Repository and then we can just swap it and everything should work X as expected if we don't have to remodify refactor a lot of code you simply refactor a repository and then it should propagate to all other domains and just work so inside domains let's create a new folder called customer now customer will hold our repository for customers and we will Begin by creating a file called repository dot go and it's inside the customer package and we will add a few errors which are exported to the users so we have whenever a customer is not found for instance we will export that error the customer was not found in their Repository import errors so we don't get any warnings let's create error fail to add customer and let's have update customer maybe whenever they try to update the customer we will return an error and we will create a interface now the interface is the Repository so let's go ahead I'm going to be naming it customer repository um you should find a nice naming convention which is explanatory I'm not totally fond of having a repository inside the name but it works so we need to define a few functions that are required to be part of a repository for customers and we want to be able to fetch customers so let's have a get and we know that the unique identifier is a Google uuid so let's accept that as input and let's return the aggregate Dot customer so whenever we have a structure that has this function they will be considered a repository so it doesn't matter if the customer is fetched from mongodb MySQL in memory whatever it doesn't matter as long as we can call the get function and get what we want it's fine let's also be able to add customers so let's from the aggregate customer return and error let's also be able to update so also return an error so this is our repository for fetching customers and managing managing them so now we actually need to implement a solution we have only created the interface which tells us how a repository should look let's go ahead and create a interface for it that fulfills the interface so I will be creating a new folder called memory which will hold our in-memory solution for the repository which we can use to kind of um maintain and test things in the unique test and building a park basically so let's create a new file I'm going to call it a memory.go so package package memory is in memory implementation of customer Repository I hope this makes sense you will probably see very soon how it all aligns so let's create a new type let's call it memory and let's call it memory repository to make everything super clear now the memory repository will hold a map with usual IDs as keys because we want to be able to easily fetch the customers and the values will be customers and we will also protect this with the async metrics for now now again it goes to fetch the wrong uuid package so I'll fix that so we have the type let's create a Factory always a factory function the factory function will turn a pointer to the memory repository because we want to be able to update the memory Repository and the memory repository will hold a map of uuids and customers so we can create and a memory repository now we also need to be able to get add an update so let's add functions for that get and we accepted a user ID and we return aggregate customers and an error let's return everything as empty values for now we just want to fulfill the Repository and the add function accepts a customer let's return nil and the final function is update so let's have that and again we accept a customer we return an error and we return nil we just want to fulfill the interface so before we move on we need to know how we can fetch the customers now we need to add a way to retrieve data from the aggregate so for instance we need to be able to retrieve the ID from the aggregate so let's jump back to the aggregate customer and let's add some functions because they are not accessible as you see so inside the reposit memory repository we cannot in our git function for instance we cannot do um memory repository customers Dot and there's nothing we can't do person.id because person is not accessible so we need to fix that so let's go ahead let's go inside the customer let's go down let's add some simple functions so if we have a pointer to a customer maybe we have a get ID which Returns the uuid and from here we can reach the person ID so let's return the ID and maybe we can also set the ID just freestyling a little bit so let's set the uuid and if C dot person is equal nil we will kind of create a new person with the ID and a c person ID is equals to the new ID maybe we should be able to update and set the name and so let's add a getter and Setter for those as well so set name allows us to modify the name again let's make sure that the person isn't new if it is we will just simply create you could return an error here like if there's no such person or ID you know return on error uh let's see I did something wonky so let's change the name this is just an example but I'm pretty sure you get and understand that the data is not accessible from outside of the aggregate nothing outside of the aggregate can modify the data this is done by exposing functions that allows others to do it so if we should be able to modify the name we expose a function which allows you to do that you don't directly go and modify it so let's in the get name let's just return the name so now that we are exposing data from the aggregate we can continue on the memory Repository so let's go ahead and fix the functions to actually manage this so inside the memory here we can do if customer okay we can go to the customers and we can check if the ID is present if it isn't present if it is present we will return the customer simple as that so let's see right we didn't name it so I'm adding the name so we the get function is receiving an ID we're checking the customer's map for that ID and if it's present we are returning the customer and if we don't find the aggregate or the customer we will return a custom error for not finding it so let's go to the Repository file and let's return error not found so the error is defined inside the domain of customers the customer domain has a error for when it's not found and the repository is simply using that error to tell you that there's no such no such customer so whenever we are adding a a customer let's go go ahead and check that the customers um if it's nil we want to create the map now the factory function now the factory function should have protected us against a empty customer's map but for extra safety I'm just going to go ahead and make sure that the map is actually created uh whenever we try to add a customer and if it isn't we will create the map so we're um locking the we're locking the structure and we're unlocking it whenever we're done and adding it so once that's done let's make sure the customer isn't already in the Repository in the repo so let's go ahead and check so if memory repository customers and then we are getting a customer as input so let's name him C so if C dot get ID so we know that the customer should be stored with their ID in the map so let's grab the ID check if it's present inside of the map and if he isn't let's simply return a error and we can wrap this error we can do the customer already exists for instance let's wrap them and let's add failed to add customer so we are returning an error to the users that they failed at because the customer already exists now if he doesn't already exist let's go to the customers let's get the ID again and let's say that it's and let's allocate the map to that and then let's go ahead and unlock and return nil so the add function is pretty simple we check if the map is initialized if it isn't we do it we check if the customer already exists and if it doesn't we add him so let's go ahead and do the update which is fairly similar to the adding now we're going to check if he is available so let's check if get ID exists so if the customer exists or if it doesn't exist let's return an error so customer does not exist we can't update something that doesn't exist customer and the customer error that we will return is error update customer and if the customer exists let's just go ahead and in this simple example we will just overwrite the current allocation so let's unlock and return it so we have made a really simple implementation of the customer Repository it's going to store the customers in memory but that doesn't matter for now and we can really start working with this now so let's go ahead and for our memory repository let's go ahead and just quickly make a few tests so we know that it's working as expected so let's go ahead function test memory get customer for instance let's make a quick test for that so we have a test case for this and we're going to have a name we're going to have an ID which we search for and we're going to have an expected error and foreign to test this we will be creating a new Repository and we will try to add a customer to that Repository so let's go ahead and say that we have a cause gimmer and it will be a aggregate new customer we will call in Percy we will check that the error isn't nil if it is we will actually do a fatal because this is the initialization of the test foreign so let's go ahead and add the customer to the repository so we need to create the repository first memory repository I'm not going to use the factory function here I'm just going to manually create it and the ID will be the customer so we're creating a new customer we're creating a repository ID we're assigning the map place of ID to customer so let's go ahead and create some test cases and it's going to be name no customer by ID so let's go ahead and force a uuid so we will do must parse and I have a uuid prepared so I will just simply copy paste that [Music] um and let's say the expected error will be customer not found so whenever we're searching for a uuid that doesn't exist we expect the repository to return a customer not found and let's have a customer by ID the ID will be ID that we created before maybe I wasn't totally uh clear what I did here I did create a customer up here to know in advance on Legit ID so we can search for that user inside test case yeah so let's go ahead and simply Loop over the test cases and we will do t-run name we will execute a test function and that test function will just simply use the repository and get the ID and if errors isn't what we expect it to be we will simply print that expected error um but we got let's see expected error but we got that error so real quick we have a memory repository it allows us to add Aggregates and manage them we can get them Adam and update them and in our unit tests we create a customer named Percy we create a memory repository we search for an ID that doesn't exist and we expect a error and we search for an ID that does exist and we expect that to also work so we can go ahead and run our tests and we should get a 200 everything's all five so we have a in-memory solution which allows us to manage customers the customer aggregate so we have a first repository in place I hope it makes sense what a repository is it's used to manage aggregates remember to keep your repository related to your domain and in our case the customer repository only handle customer Aggregates not more we don't want to start coupling things inside the repositories we won't lose coupling so one repository handles one aggregate remember that but let's be honest the real in the real world we won't be able to have a whole logical flow inside the tavern where we rely on One customer repository so we have to start coupling some somewhere for instance if we have an order we need to get the customer make a billing and stuff and that brings us to the next point of domain driven design so we have all these entities we have Aggregates and we have repositories to manage their aggregates but it doesn't really look like an application yet does it that's why we need the next component which is called services now a service will tie together all the Loosely coupled repositories into a business logic that fulfills the needs of the domain so in our Tavern we might need an order service an order service is responsible for shaming together the repositories that performs an order so getting the customer with the customer repository getting the product with a product repository for instance and then making the order making the billing so you have a billing service for instance uh and the service kind of takes these Loosely coupled pieces and couples them together and we will be implementing an order service in our Tavern so we can start making orders so it's not an aggregate so let's go ahead and create a new folder called services and the services will hold a order.go which is a order service and let's call it package services now whenever you have Services the factories tends to get larger and more complex because you accept multiple repositories as input for instance and one trick I've learned when I was reading John Calhoun's book about web development was a sort of a service configuration generator pattern and we will be using that here because it's really allowing you to create flexible modular Services where you can replace the repositories really easily so let's have a look at how we can do this so whenever we have a order service we're going to create something called a order configuration the order configuration is an alias for a function signature which will accept the order service and return an error so let's go ahead and create the order service so we actually know what's going on so we have the order service and the order service will have a customer Repository because whenever whenever somebody makes an order they are a customer so we need to handle the customer aggregate so we need the customer repository in the service so we have a order service which and we also have an audience for this so this all yes it's a little bit complex but let's let's make an example and you will probably see and hopefully understand inside the factory we're creating a factory function for our service and we will accept a variable amount of order configurations and a configuration is a function which takes in a pointer to the order service and returns an error the reason why we're taking a pointer to the order service is because we want to modify the service based on the configuration so let's go ahead and return the order Service as a pointer and also an error so we begin by creating the order service inside our Factory function and it's a pointer to a order service now we will Loop through all the configs and apply them so let's go ahead Loop through a range through the configs and in here for each configuration we will simply execute the configuration and insert the order Service as input we will check if the error is not nailed and return nil if and return error if it is let's return the order service and a nil at the bottom so I don't know if this makes much sense yet but hopefully we will soon see how we can leverage this to customize the order service really much now for smaller services this approach might seem Overkill but it's really cool when you start building and having a lot of configurations that are accepted so I'm going to go ahead and create a order configuration which applies a customer repository to the service so inside here we're going to create a function called with customer Repository and it applies a customer repository to the order service and let's call it with customer repository and it accepts a customer repository as input and it returns a function signature of order configuration so let's explain this we return a function that matches the order configuration alias and we need to return a function so we can chain this so let's go ahead it's it looks like this and it modifies the order service the order service holds a repository inside the customers field so we do order service.customers is equal to the customer repository that we received as input and we'll turn nil nothing goes wrong here not much to do we can create a second um function which would be with memory customer repository for instance and it accepts no input a parameter but we don't need this can change we don't need to accept the same inputs because we return a order configuration function so we don't have to care about how many inputs we need and if we do the memory customer if we do the with memory customer Repository we will create a new memory repository and we will return the previous function with the customer repository as input which will in turn return the function which will allow us to say that this is a configuration hope that makes sense let's let's take a look at why we would do this why would we go through doing all this well it's because we can now do whenever we are creating a new order service we can do New Order service with memory customer Repository for instance now this code would return a order service with a memory repository applied now you have probably seen this before uh and a a good example with Full Features would be like with logging and maybe the log level would be debug for instance this is just examples but you you probably understand with um maybe we want tracing and we have a configuration for that uh so we would have something like Jager which traces everything that's done in the order service we could maybe Implement that through a function like this which would apply a tracing to the order service now the reason why you want to do this is because whenever you in the future change from in memory to my SQL and you have this whole software and the order service is used in a lot of places you don't want to refactor everything you would simply build your repository for MySQL or mongodb or whatever and then you replace the simple configuration here with with customer repository for instance and everything will just continue working as long as your repository is working as expected so it becomes super scalable super modular and just really amazing so I really hope that makes sense it's a bit it might seem a little bit overkill for smaller services but I've seen I've had projects with like 10 or 12 repositories in one service and this has really been helpful when you reach that when you reach that stage it's really super helpful to be able to replace certain parts of the business logic maybe for a unit test or integration test maybe you have a mail service but you don't want to send real emails you can replace the repository for the emails as an example so let's begin adding a few functions for business Logic for the service so I'm gonna go ahead here and do a pointer to order service function and let's create order and order is going to accept the customer ID which is again a Google uuid and we will accept array of IDs which will be products which they order so whenever somebody makes an order they hand in a array of uuids and we will Begin by fetch the customer so the service has access to the customers through the customer repository so let's do customer and error equals the order service customers dot get and we're passing the customer ID what will happen here is that the customer service will use whatever customer repository is applied in MySQL mongodb memory it doesn't matter whatever we have applied through a order configuration it will use and it will call the get function for that repository in our case the memory repository will get whatever is inside the customer's map now if we don't get anything here we will return error and we will return nil here and what we will do here is we will simply print line the customer now whenever somebody makes an order you would probably want to get each product and we need a product Repository we have no product repository so we need to create one well yeah and then after we created the product repository let's create a order configuration which applies the product repository to the service so the product Repository will go here we're in the aggregate so we need to create a product aggregate first remember a product and a repository has a one-on-one relationship if we have an aggregate called Product we need a repository to manage them so we need both pieces so let's go ahead and create a new aggregate called Product and the product will be a we will need a entity a root entity because this is an aggregate so let's go ahead and entity dot items because a product will have a root entity and that root entity will be an item a product also contains a price and a product also contains a integer so this is our product aggregate now we need a few few functions to expose the information first off we need a factory and it will accept name description and the price it will return a product because we're creating a product so let's go ahead and see is the name empty is the description empty and return product and maybe return error missing value so let's also create that error saying missing important values is this should be very much more explanatory than what it is right now but we're not learning how to write errors here so let's return a product and the product will contain an item seeing as that is the root entity and the root entity will have a unique identifier it will have a name and it will have a description now let's also return nil so we also need to assign the price and maybe the quantity which would default to zero so we have a factory for creating products and we also want to be able to fetch the data from the product so let's go ahead and do get ID which Returns the uuid so P item.id which is the root ID and let's go ahead and do get item maybe so we can extract The Entity from the aggregate that's a plausible scenario so get item will return the item from the product and we can get price for instance I mean this is basically will change depending on what your aggregate holds what kind of data your get hold and what you need to expose so we have the product I could get in place we can reach data inside the product Aggregate and we can create products let's go ahead and inside domain create a new domain which is product and the product will hold a repository and we will Define the product repository here so we will have product Repository and it's an interface of course so the product repository will manage and handle the product aggregate oh Let's see we might need get all get all will return an aggregate a slice of products and we need get by ID maybe so we can fetch certain products and we will pass in the uuid and return whatever aggregates we get or an error if there's no such product we can add products of course so let's add a product which will simply return an error we can update update the product this doesn't have to contain all these functions it's basically up to what your repository needs to be doing but all these functions make sense when we handle products maybe we can maybe we can delete products as well so let's settle for these ones um so we have the aggregate we have the repository we just need to create a structure which fulfills this product repository so let's go ahead and create a new folder I'm going to call it memory again because this is the memory Repository for the products foreign memory that's basically this will be very similar to the customer repository since it's used to just you know store it in memory for now in a map so we will do products which is a map uuid Dot uuid and we will store the aggregate products and of course we want to protect from concurrent rights so let's add a mutex and by now you should really understand what we need to do we need to create a factory so let's create a factory which creates the memory product Repository so let's return a pointer to a memory products Repository let's initialize because that's what the factory does to initialize this and then we also need to fulfill all the functions inside of the repository so let's go ahead memory product Repository which has a get all which should return everything inside the repository as a slice so let's do aggregate dot product and we return our error so we will need to convert the map into a into a slice and so product range NPR let's range over them and let's do products append and the products and return products in this case we will never return our error but it's up to the interface to determine if it's possible it's not up to the implementation it's up to the interface so the repository decides if we need to return an error or not if this was to be replaced with a mongodb instance we would we would need an error because the connection could fail or whatever so let's go ahead and just Implement a few of these functions so we have get by ID which is the ID and we return an aggregate products and inside of here we checked If the product is present inside the map and so let's get by ID we are passing in the ID so let's just check if it's there and if it isn't we return the product if it is here we will return a one of those errors that we created or we didn't create any errors maybe product Dot okay so we didn't create any errors which we need to do I'm going to create a product not found error inside of the product Repository foreign let's just make it say no such product and let's import the domain product let's import the error so that's over the product package so we know what we can return so let's go ahead and keep continuing with these functions we do the updates update and it's um aggregate product and it returns an error so we do kind of need to empower lock we can defer the unlock um and we can check if there is a we can check if the ID using the get ID if it's not available and if it's not available if it is present we will simply overwrite get the ID from the update function say that it's actually assign it to that let's assign it and let's return nil and we also want final function we need to be able to delete and let's say uuids we will delete you your IDs whenever they are passed in and very similar to the update function we will lock and we will defer unlock we will check if the item isn't present if there's no such item present we can't delete it and let's go say product not found if it is we can simply call the delete on the products and the ID as key and return nil so this would be a in-memory implementation of the products and now we can go ahead and return to the order service and the order service needs a product repository which we import oh look at that front cut uh let's change that to Google I made a typo so let's rename that to product Repository right so we have customers and we have products for this order service now a service can hold multiple repositories but a service is also allowed to hold other services so a order service could in here hold a billing billing service for instance so we wouldn't have to re-implement the billing service if the order needs to bill for the order we could simply hold a sub-service inside a service but before and not to confuse you let's keep doing what we do so let's add a new order configuration function so with memory product repository this time and it accepts a array of aggregate products and it returns a order configuration same as before so we begin by returning that function and the function signature is to always return the service as a pointer and an error of course if there's something that goes wrong so let's create the Repository so let's go ahead and import the product repository I'm going to use an audience for this now front map and let's change the domain from customer to product so inside here we will do create a new frontmem.new which creates a new product memory for each and every one of the input products we will arrange products and we will do if error equals product repository add didn't we add we didn't add an ad my bad let's go ahead and create an add function also I miss that so let's go to the memory product and let's do function MPR memory product Repository let's add and let's accept a product as input and return an error and same as update and delete we will lock and we will unlock when we're done we will check if the product is available already by getting the ID from it and we'll check and let's return product and let's return error product already exists so let's go ahead and also quickly create that error and it will say theories already such a product so if we don't have that product already we will do a new product get the ID as key and insert it as the value and return nil thank you so now inside the order service we can go back and we can add the products so let's go to PR ad product and if error isn't nil we will return the error otherwise we will simply keep going and then the order service dot products will be assigned to the product repository and let's return now so whenever we create an order service we can pass in so an example would be New Order service with customer Repository and with memory product repository for instance which would allow us to create the order service depending on our needs and whatever repositories we want to use right now foreign so inside the create order where we left off we have no product repository we said but this time we do so let's go ahead and keep going so we do have a slice of products and we do have a price so let's go ahead and loop through all the items that is being ordered which is stored in the product IDs let's fetch those IDs from the product Repository and we can do get by ID and we can insert ID so let's check if there's an error if there's no such error we return 0 and an error too many times we don't want to return zero we want to return the only the error of course so we are fetching the products based on their IDs from the Repository now we want to append those products to their products and products slice that we have up here which is the products that has been ordered and we also want to update the price so let's do product get price to update the total price maybe we should call it total instead so we're getting the products we're getting the total and let's just simply print whatever they have ordered so customer bam has ordered products and let's get the customer ID and length of the products he ordered Maybe so we are getting the order we're using the customer repository to find the customer we're using the products repository to find the products and to test this let's go ahead and make a unit test which creates the order service and execute um the create order function so I'm going to go ahead here and do order underscore tests of going to be inside the package let's first create a simple function called init products and let's do testing and it will return a slice of products this is used to Simply initialize tests easier so let's go ahead and do we want a product which is a beer we want our Tavern to serve beer so beer and the description is a healthy beverage and the price is 199. so let's check if there's any errors and let's just simply fatal [Music] um right so I can't return that of course let's simply call um the testing fail if we fail to add because we don't want to continue our tests if something goes wrong in the initial initialization of the test and maybe for the beer we want some snacks and they are all 99 and if the error isn't nil let's go ahead and fail again so I have peanuts we have beer and maybe we should have some wine and aggregate new products wine and it's a nasty drink I don't like wine so if the error isn't nil again let's execute a fail to stop all the tests um and let's return a aggregate.product and inside we will have beer peanuts and wine yeah we need to finish with that so init products will create a simple array with a few products for us now let's go ahead and create the order Service Test so test order new order service and inside here we want the products so let's call the init products and passing test so it can fail now we want to create the order service and we have the function for it the factory function which is New Order service New Order service accepts the valuable amount of configurations but we do know we need at least a memory customer repository we need a memory product repository and the memory product repository except the products as input so let's pass that in let's see if there's any errors and let's again let's do a fail this is not the best unit tests I hope you see now here how cool the configuration pattern is where you can simply pass in functions that modifies the behavior of your service using these repositories kind of makes it really really really easy in the future to change the whole behavior of the service which is really nice when you're refactoring or changing and any vital part or infrastructure Parts it's really handy so let's go ahead and do a new customer and I'm going to call him Percy and let's do T error and let's add the customer to the customer Repository uh right now I'm creating the customers using uh this but we need to add them to the repository so that the repository knows about them so let's go ahead and do that repository resides inside the service so let's go to the order service dot customers and let's add him let's again check if there's any errors so once here let's go ahead and do a order we need to create a order and for that we need a slice of uuids so I'm just going to go ahead and order a beer which is the first index in my case and let's go ahead and do or order service dot create order I'm going to pass in my customer ID I'm going to pass in the order that I have and let's check if something went wrong thank you and let's do that let's see what he complains about cannot assign one all right my bad so let's see the create order uh is executed and let's let's try it out and we're running the test and everything is okay so we can even do a let's go ahead and do a let's do a debug so we can see the output also and whenever we do the debug we get the output so we can see here customer blah blah has ordered one product which is really cool um so I hope I haven't confused you too much yet entities and value objects they are instances and entities are changeable mutable and value objects are not Aggregates hold multiple entities or value objects but they are related to one root entity you have the repository which manages the aggregates and you have Services which combines and ties together the repositories so it's time to test the final thing the final piece of this puzzle we need to create a Tavern and the tavern is a service so basically inside the services I'm gonna go ahead and create a file called Tavern because that's what we're set out to build from the beginning in this file we will be a service and we will create the tavern structure and the tavern structure will hold the order service because we want to take orders inside our Tavern so order service and it will hold a pointer to a order service so this is the tavern will be a service and it will hold sub Services as explained before we can do this and in here we also want to maybe have a billing service so we can accept payments or whatever how we want to structure that is basically up to you and your application um basically let's just add a billing service and let's make it an interface for now um just to make this really simple and again I will use the same pattern as before I will create a Tavern configuration which is a function signature which accepts a pointer to a table so we can modify it and returns an error now the factory will be new timer we will accept a valuable amount of Tavern configurations and we will return the finished version of The Tavern configured and done and an error if something goes wrong so let's go ahead and create the timer or a pointer instance profit and let's range through all the configurations and let's apply them config input or Tabor and if error isn't nil return error and new so we accept a variable amount of configurations we look through them we apply them to our instance and if nothing goes wrong we return the configured instance so again the tavern service will have a with order service function which accept a fully configured order service so we will turn your Tavern configuration so return the function which is the signature which would be a tether and in here we apply order service to be the order service that we can pass in and we turn nil if nothing goes wrong let's also create a simple function we have the tever we do order customer a user ID remember we need a new ID and we need products to make an order uuid and we do error so at this point of time it doesn't make much sense let's go inside the order service again and make a small adjustment I do know why I wanted the order service to return two values let's go ahead and not accept the output value and it will complain and let's go inside the order here and let's return a float 64. so let's return the total from the order whenever somebody orders something so let's go ahead and in here we return total so let me close this we don't need this open right now so whenever somebody orders we calculate the total price and we return that from the order service and inside the tavern we do get the price so let's go ahead go to the order service we create the order we're passing the customer ID we're passing the products that they want to order we check if there's any errors return error and build the customer let's see what we should build the customer whenever our service robs return nil let's see what it says we do printf instead we can pass a new line in the beginning we can pass a new line in the end just to make it a bit clearer so our order service except orders using the or or tabber accepts orders using the order service but we can then continue working on the order for instance billing the customer and shaming cause shaming these things makes it very flexible again I'm gonna go ahead and make a test file to test this out uh package services I'm gonna do test the tavern make some file testing here so again we're gonna edit the products because we need products like that and let's create our order service using the order service Factory so we need the customer repository and we need the memory product repository which accepts the products and let's see it should be a memory customary repository instead so we are creating the order service let's just fail if something goes wrong now that we have the order service which we will use as a sub service to the tavern let's initialize the Tapper it's a new tabber with order service and passing the order service again we can check if anything goes wrong and now we can now we have a Tavern so we need to generate a customer so we can test this so let's go ahead and create a new customer and if anything is wrong again let's just fail and let's make a little order this is not how I would make a real application make orders passing a list of but you get the idea so now we have the tavern we have the customer we have the order let's let's try it let's do Tavern order and let's get the ID and let's pass in the order and if the error isn't nil let's go ahead and do a fatal and let's see no new variables of course sorry so let's go ahead and debug this and we should see that the customer was not found in the Repository uh of course because we are only creating the customer here we need to go ahead and do uh if error order service customers add let's add the customer to the Repository and if there's any error let's simply do a fatal let's debug again and now you can see we're using the order service to make an order and then we're getting the price back and we're simply the Tyrone is simply logging that value right now but you could pass the value into the next service or another repository and basically how you want to do that now at at this point of time we are only using in-memory repositories but hopefully you get the idea that we can easily replace these repositories with another implementation I work to create a mongodb implementation of this instead I would simply be able to go inside the the factory function here for instance so if we want to use the mongodb repository instead I would do with customer repository for instance and but if we Implement that function we can simply replace the factory function call with the repository and everything will continue to work that unit test will continue to work even though we replaced the in-memory solution with the mongodb solution this is really great we haven't talked about how you would structure your data yet because I said before that we don't structure the data in here you would do that inside of the Repository so basically if we have a memory repository phase that's we determine how to store the customers in that Repository and if you have a mongodb instead you would determine how to store the customers inside the mongodb which format to use what the column or Fields would be in MySQL that's up to the repository and that should never be Spilled Out or affect any other service they should always confirm to the aggregate model that we have in the code but then it can change depending on the database of course but then it's up to the uh implementation of the repository to confirm and convert back between the format whenever it stores it and when it passes the customer out it should convert it back so now that we have the whole Tavern in place I think it's actually time to implement a repository to show you how easily it is to switch a repository and have the service still functioning correctly so inside the customer domain I'm just going to go ahead and create a new folder called and will have a file which package so I'm gonna go ahead and create Repository struct and we will contain a mongodb database let's see database let's import that the driver let's go ahead and do go montidy to fetch that I often do go mod tidy instead of goget seems simpler to me so we will have a collection for storing customers so I'm gonna go ahead and add that now let's make a super simple customer struct inside the remember the customer is an internal type that is used to store a customer aggregate inside this Repository we have to use an internal struct to avoid coupling so this implementation shouldn't have any coupling to the aggregate so that's the reason why we have a totally separate um totally separate structure inside the repository package so I'm gonna go ahead and make sure we are maintaining a uuid because that's something that we need to maintain and we are going to store the name in this case we're using the piece and tags because it's mongodb so one very common approach is to have some sort of converters between the formats so since we're not operating on Aggregates straight away inside this we can sort of have these helper functions that converts between the formats for us so I'm going to just go ahead and have new from customer which is a function that accepts the aggregate.customer and returns a customer so we're going to create the customer and assign the correct values using the get IDE and using the get name like that so now we can easily go from aggregate to a local internal um struct which we will use and then of course we will do the other way around so one converter and this is this is probably the the thing that I like least about this approach because it's a little bit overhead when you switch between the formats but again it's really easy to implement it doesn't take a long time to have these four matters and and you get kind of much from it having this loose coupling it's really nice when you're doing a refactor so let's go ahead and create a new aggregate and let's do set instead of get this time and we have the ID in that field and set name is present there and let's return the customer creator so those two functions will handle that for us of course if you have larger structs they become larger but should be fairly simple to implement so what we need is a get add and update function and now to save some time I'm not going to implement the update stuff and I'm just gonna have the functions to comply with the interface so let's go ahead and the new will create a new Repository we will accept a connection string because that's how you connect using and in here we will create a new client mongo.connect context let's add some options options let's do apply Yuri and then the connection string and let's check if there's any errors again let's return those errors so once we are connected we can fetch the database yeah let's call it DDD for this purpose and let's go ahead and have a collection called customers and then let's return that Repository and we're going to save the database and we're going to save a pointer to the customer's collection so we can easily switch and let's see mixed name oh my bad connection string we need to make that a string of course so we have the connect function in place so let's go ahead and create a simple let's see repository we have a git function and again we accept the uuid return on aggregate and get that customer and an error and here we will then query the database for the value that we're searching for context background 10 times a second defer cancel so let's see let's do let's go to the collection let's do find one that's passing the context and a piece on M and what we're doing here is that we're searching for this particular ID and let's import let's Marshal the results into a Customer because that's the format that we will be storing the values in so let's decode and then again value checks and I'm going to return on empty Aggregates and error and here we're going to call the helper function to aggregates and new so the let's see or what is it complain about it's only used okay sorry it's my linter if error you can do this instead and this is shorthand syntax in go for if error so we find the customer if we decode it and we return it as an aggregate otherwise we return an error and we also need to be able to add to the database so let's go ahead and do aggregate add and we return on error if anything is wrong and I messed up have the camera blocking part of my screen which I'm good sorry about that so let's see again let's create a context and it's going to be with a timeout and we're going to set that context to 10 times seconds we're going to defer cancel and then we're going to create a internal format so if we accept the aggregate we convert it to the internal format and then we will go ahead and insert that into the collection so we can return error otherwise we can return nil so for the update part I'm just going to go ahead and do a panic because we're not going to use it so let me just go ahead and do to implement and now we have the repository in place we can backtrack and go into the order service and the order service we will create a new function which is similar to with memory customer repository but instead it will be with customer Repository and again it will return order configuration but let's go ahead and initialize instead of memory we're going to do and we're going to accept a context so let's go ahead and do context.context and also a connection string and we're going to pass those inside mongo.new which will return an error if something is wrong so we need to actually be able to return the error so I'm gonna go ahead and do it like this instead so we're returning the function so if this error is not nailed we will return that error and we can go ahead and do return new we don't need to use that wrapper function just go to order service customers equals the customer repository return nail so now we have a function that helps us configure instead of a in-memory Repository so remember the unit test for the tavern we have it here we initialize a product and we sort of add everything to the repository and this works when it's in memory however if we change this now to instead run with the customer Repository which will accept the context and a connection string so let's go ahead and add a connection string to my localhost 2707 and comma so if we add the connection string this will work if we have a up and running this will now work as well as it did with the memory repository it would be the same thing for service the service won't have to care about what repository it is we have configured the repository it will trust that it fulfills the interface otherwise the compiler will complain and if we were to execute this now and we will get the same result as we did with the in-memory repository which is pretty nice we replaced the whole customer repository with one line of code or would have to create the repository of repository of course but we can replace Parts really easy and it's just really really really amazing so in this article we have covered the basics of domain driven design in short we have covered entities which are mutable identifiable structures we have covered value objects which are immutable unidentifiable structures such as transactions without an ID we have covered Aggregates which is a combination of entities and value objects stored in a repository repositories are a implementation for storing Aggregates and other information we have covered factories which is used to create and help create complex objects by creating a new instance easier for that developer and other domains so new memory repository for instance we have covered Services which is a collection of repositories or sub services that builds together the business flow remember in this example we have named everything after what it is so we have a aggregate and we have a customer domain and we have a product domain we have entities folder which contains our entities and this is not how I would do it in a real project if you want to this video is more about learning what all the concepts are if you want to see how to structure a domain driven project instead I can recommend you my other video about that how to structure domain driven design in go which is a lot shorter I promise there's a lot less to cover in that one so I will leave you up to implement a one great exercise for you could be Implement a billing service but I do recommend you to train a little bit with it and I hope it I hope this makes sense I know it's a lot to take in and I really hope I narrowed it down for you you can find the full code for this project inside GitHub where you can check out how I did things and hopefully you like this video so feel free to reach out to me in any possible way like And subscribe this video if you liked it of course and I love feedback so if you have any feedback feel free to reach out thank you [Music]
Info
Channel: ProgrammingPercy
Views: 33,924
Rating: undefined out of 5
Keywords: ddd, domain driven design, eric evans, aggregate, entities, services, repository, repository pattern, go, golang, factory pattern, software architecture, software, programming, coding, design pattern, software design
Id: 6zuJXIbOyhs
Channel Id: undefined
Length: 117min 42sec (7062 seconds)
Published: Thu Dec 08 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.