Transcript
Alagarsamy: For those of you who don't know NServiceBus, it's a messaging platform, a software for .NET that enables event-driven architecture, and it's built on top of existing queuing technologies like RabbitMQ, Azure Service Bus, and so on. For me, event-driven architecture and domain-driven design are awesome things, and when you put those two awesome things together, you can get really cool services that are autonomous. To me, the whole deal of trying to have small services is so they could be autonomous, so you can scale, so they can be reliable, and so on. For me, combining the domain-driven-design discipline and the event-driven architecture as a technology gives you the best of both worlds.
Before I started particular software, several years ago, I worked for this company, and the core domain of this company was sending regulatory notices when homeowners defaulted on their mortgage payments. They did this on behalf of banks and mortgage companies. I joined the company in 2010. Business was ok, but in 2010, the software couldn't keep up with the load. You can understand why, because in 2010 is when the real estate meltdown happened and the core business was about sending all these regulatory notices, and so the software couldn't keep up with the load. That's how I started getting into event-driven architecture to see how to scale, and that led me on a journey of event-driven architecture.
My family and I, we buy iPhone cases because I am the iPhone killer of the family. My kids presented me an iPhone cell phone case for Christmas. It's true, I still have it. If we look at trying to model this page, we see a lot of things. We see pictures, name, description, price, and so on. If we wanted to model this in our software, we tend to have a model for it. This is years and years of practice, and we try to look at all the data that goes together, and we come up with this model. The trouble is, what is a product?
The definition of what a product could depend on who you ask. For a person in sales, it's a thing that is marketable, that sells. For them, the important thing is how can they market it. For a person that's in inventory, it's a thing whether you have or you don't have, and that's their concern. For a team that's in shipping, it's a thing that goes in a box that goes from one place to the other. Depending on who is on the team, different teams tend to use slightly different language to describe the same thing.
Unified Models and Bounded Context
Language becomes important, and when you try to fit all of this in the same model, it becomes cumbersome. It becomes complex and unwieldy, and you have to deal with duplications and contradictions.
Let's say you're buying a MacBook Air. You're wondering about the weight of the laptop. For the sales team, it's a big thing, and they want to try to market that as the lightest laptop or whatever, Apple is very good when it comes to branding. For the shipping team, it's, "How much does this weigh?" When you have a model, and in this model where you have one attribute called weight or something, the team in sales could use that same value, and the team that's in shipping could say, "I see there's already attribute for weight. I'm just going to use this." What's going to happen is like you might have some weird use cases where the value of the weight appears differently because the shipping team did some updates. Now you're going to start having weird behavior on some use cases. Your system could act very contradictory, and these sort of things are kind of hard to debug.
One way to get rid of that contradiction is having explicit fields, you could call it display weight, shipping weight, and whatever. Now, what you've ended up doing is, you slightly bloated your model. Not only that, the domain team, they are not calling this as display weight or shipping weight or something. To them, it's just weight. By introducing these different terms in your code, you've lost the language of the domain. Now, there's going to be a difference. When you try to communicate this concept with your domain experts, you're now going to have to, "This shipping weight means it's this, etc.," so a lot of translations in your head, and so it becomes hard to communicate this as well. Therefore, unified models are not the best way to go.
Domain-driven design helps us to understand this, and has this concept called bounded context. Who here knows about bounded context? A lot of hands, awesome. By understanding that you don't have to have one model to describe everything, by understanding that you're going to have some different spaces - I tend to think of a bounded context as a safe space. A team has the space, and in the space, the models that live in that space, they can evolve freely. It uses the language of the domain. The models in this space are logically consistent, and moreover, they tend to carry the language of whatever the domain experts call. The sales context can have a model for product, and that can have weight. That could be completely different from the model in the shipping context, which can also have weight. They don't contradict with each other, or there's no duplication.
This allows us to have clarity in your model, and also gives you freedom. For the team that's working within the context, there's clarity in the language, and there's logical consistency in your models, but for teams that are working outside of this context, they are not dependent on your schema or something to make the change in their models. They're completely and entirely independent. You can evolve things differently and together. To me, that's a huge advantage of bounded context and how they provide clarity and freedom. The question is how do you then find this bounded context?
Finding the Boundary
There's a lot of stuff in how do you identify the context, and that's the tricky part. There are several ways, to me this is really the tricky or the hard part. I use some ways as a guideline. One of the things that I do is I ask myself, "Do I need to have transactional consistency when I try to update a couple of fields together?" For example, would I ever need to update the product description and the availability in the same transaction? Probably never. If that's the case, there's a clear indication that those two things do not belong in that same context. I use that as one heuristic.
Do we need transactional consistency for updating the two fields? If not, then we don't have to. The other thing is you could split according to the departments or teams, because if the teams have a natural boundaries or rules and business behavior that they are trying to each accomplish, then that might be also a good start. You could start there and see how it goes. Of course, things are going to evolve, and the boundaries are not just like fixed lines. It's going to change based on conversations that you have with domain experts. You might find by having a conversation a concept that is not well-defined in your model, or you might find that that doesn't quite work well together, and so you might move that to a different context. These are some of the things that you're going to learn by having conversations. The whole idea is, as you learn new things about your domain, about the behavior, about the rules, you try to take that knowledge and then see how you can factor or refactor your existing models to fit that behavior.
In this case, you've got three contexts, sales, inventory, and shipping. In each of these different contexts, you can have a model for product, which is all different. You can have the same attributes, and it doesn't matter. By having this, you're using the language of the domain, and the domain people can also understand when you refer to your model as whatever property. You can say, "What should I do when the weight changes?" You are using the same term, weight, as the same term that they know and understand, so there's no communication problem. That's why it's important to try to use the language of the domain in your code.
The other way is, you can think about looking at the business processes that the business has. One of the things that domain-driven design really pushes is, look at the behavior. The interesting part of the business is in the behavior. If we can capture the behavior of the system in our code, then our code is going to be more aligned with the business. If the whole idea is for us to write software that's well aligned to the business, if we can design our code in such a way, then when they come up with a new requirement, we can go along with those changes to add more features and so on. We just can't get there by like having one model. It evolves, and as rules change, as business requirements change, we just keep learning from the domain and try to refactor our models.
In this case, it's a business process. For example, Amazon gives you $70 off if you buy the prime card. Obviously, there are some business rules that involve you buying a prime card, and it involves something in the shipping context and in the pricing context. Now you can see that there's a lot of participants. You can start to ask yourself, if you wanted to have transactional consistency, what things need to be together? You can start to form your models, and then you can see where it might fit and so on.
Splitting the boundaries is a talk by itself. My company has given me this link. You can access this, this is Udi's advanced distributed design course where he talks about finding boundaries. This is one of the talks of his course that you can have access to, which goes into larger, more detail.
How Do Bounded Contexts Communicate?
Just to get a quick check. We're at a place now, we've clearly identified unified models are not great because having one model to rule them all is a bad idea, and we clearly understand that we need to have different context, and by having different contexts, models can live in the context and evolve freely and so on. What's the point of having all these contexts if we don't communicate between the contexts? In order to have cohesive behavior for your system to be an actual system, these contexts need to communicate. To me, that's where event-driven architecture comes in. You want to have this communication mechanism in such a way, it reduces complexity, it's loosely coupled, so you can evolve it and you can scale it, etc.
To me, that's where messages and events come in. If we look at messages, you can classify them as commands or events. These are the two main category of messages that you can use to communicate between bounded context. Events are a message that conveys something of significance has happened in the business. It's used to communicate the state change from one bounded context to the other. Commands, on the other hand are convey intent, and commands can fail. That's the huge difference.
Events are something that get published, so multiple subscribers are going to receive it, versus a command is usually sent to one particular service or context or something. Typically, you want to use events as a communication mechanism between bounded contexts. You can use commands as a way to decouple or have loosely coupled communication style within the bounded context. The one thing about commands is that it can fail. Has everyone watched "300" in this room? When Xerxes sends his messenger to Sparta, to Leonidas and say, "You shall bend your knee to Xerxes," it didn't go so well. If you think about bounded context, they are two separate things, and they don't tell each other what to do. You can think about bounded context as isolated things. They do communicate but using events.
I want to take example of a business process, just to walk through how we would do this with domain-driven design and have this communication using events. The requirement is when an aircraft type has changed, the aircraft company wants to notify the passenger saying, "You have a new booking proposal." The passenger has the right to either accept it or cancel it,
This is an example Norwegian sent me, very kindly said, "Sorry, we canceled your flight to Rome, here's your new flight. If you want it, then you can take it. Otherwise, you can get your money back." A business process, if you look at it, it can be triggered by an event from one bounded context to the other. You've got the flight planning context saying that, "This aircraft flight has changed." The booking context can now receive that event and then go and act on whatever it needs to do. The keyword here is when.
When the domain experts or the people in the business starts to use the language when and then start describing something, there's usually event that follows that you can get out of. The thing to understand here is we said that messages help you design your systems to be loosely coupled. The whole point is we don't want to be coupled. However, we do have to be careful in how we design these schemas because when you send messages over from one context to the other context, you are going to be sharing the schema. It becomes really important what you put in those events and what you put in those messages. You could put a lot of information from one bounded context in that event, and now you are definitely coupled via the event.
What happens when this flight planning bounded context changes the schema of the event? How is that going to affect this booking context? You have to ask yourself and be very intentional about the data that is being shared from one bounded context to the other. Just because we use events and messages doesn't automatically give you Nirvana and happiness. We still need to pay attention to what we put in the messages and how we design the schema.
The other thing is, the business processes, there could be multiple messages that takes part in the same process. The booking context receives the aircraft type has changed event, but then internally it might need to do a lot of things, which might involve sending messages, "We need to rebook this flight, and we need to notify this customer," and "what happens when the customer said, "No, I don't want this. Go ahead and cancel this booking"? There's a lot of events that are going to participate in that same process.
When you have a lot of messages that are participating in the same process, you might have to have state involved, because based on the state, if the customer canceled, then you don't need to send the customer a rebooking email or whatever because they canceled it. Based on the state, you are going to take certain decisions, different decisions. It's important how we manage the state, and that's where we have this pattern called sagas, and it can come in handy.
Saga Pattern
About the word saga, there's a lot of contention about the word saga pattern in like the community, whether it really should be called a saga pattern or the process manager pattern. I'm going to leave that debate for a different talk. Basically, the saga pattern allows where multiple messages can take part in the same business process. I can't remember who said it, but friends don't let friends do distributed transactions. We can't have very long transactions, these are long-running processes. It's impossible to do that. The saga pattern allows you to, when a message comes in, you take whatever action you need to within that small scope of that action, you finish the action, and then you remember some state of what that happened during that part of the process. When the next message comes in, you can rehydrate the state and see, "Ok. This is how my business process is at this moment." Based on the state, I might need to take a different action or send a different message and so on.
The saga pattern is very useful. The important part is it allows you to take compensating actions, because you can't have a very long transaction spanning all these messages, you are going to have to take compensating actions when messages may not arrive because, in a messaging event-driven world, you have to accept that you're not going to have messages arrive. You have to ask yourself, "What do I need to do? How long do I need to wait?" You have all these constraints, and so you have to have state. This pattern is useful. I can't get into the details of it some more because I think there's a whole different talk on sagas and how you can use sagas to work with evolving or changing requirements.
Eventstorming
We said that, ok, events and messages are important, and we need identify what events or messages are involved in this business process. How do we go about that? There's this technique called event storming. Alberto Brandolini came up with it several years ago, and it's a fantastic collaborative technique. The way it works is, you put a long sheet of paper on the walls literally, and then you collaborate with your domain experts. The thing that makes this work is you have to have the right people in the room. You bring your domain experts, your architects, whoever is a key stakeholder off that system. You bring that person into the room, and then you start with putting up events. You talk to them, and as a first step, and whatever events that you think are part of the process, you put them on the wall. Then you have conversations, and as you have conversations, then you're going to identify the constraints. Your goal here is to try to find out as much information about the domain, about the constraints, about the pain points, and try to model that in a visual collaborative way. When I first did this, it seemed very chaotic at first, because you have businesspeople in the room wondering, "What am I even doing here? You guys are the programmers and architects, why am I here?" Then once they start putting up stuff on the wall, you can use this process to get discovery on an existing system as well.
What I saw was, the people like started adding flows and how things work in terms of events. Then, the business manager was, "In this scenario, we shouldn't be doing…" whatever the action that the programmer said the software was doing. Then the programmer was, "No, that's how this software behaves." He was, "It shouldn't behave like that." That's the kind of conversations that you want to have. The sticky notes are cheap and writing code is expensive.
As an example, in a previous place where I worked, we tried to automate this process to make it easier for this person. There was a lot of manual work and a lot of emails that come, and we had to streamline this process. One developer was assigned, and there was a guy in New York who did the UX design and whatever. It took a month, and we came up with the software and then gave it to the lady who's supposed to work with it. She just said, "This is unusable. I can't use this." Unfortunately, nobody had taken the time to ask her whether this is usable, she would have told us a long time ago and saved us a whole month of development work. Collaboration is super important, and having all the right people in the room when you're making these decisions is important. Again, sticky notes are cheap, you can throw them away, but writing code and design time takes a lot of costs.
If we were to take event storming as an example and try to apply to the example that we were trying to do, we would first try to identify all of the events that are part of this process. Aircraft type has changed is an event, and we might need to know that the booked flight was changed because we want to try to get a new booking. Whenever that happens, we want to publish an event saying that thing has changed. If a customer is not happy and cancels it, you want to have an event for that. You try to identify all of the events that happened in this process.
Sometimes it's easy and you can tell, "These events might belong in the flight planning context and these events might belong in the booking context." Sometimes, you can tell. Either way, you have all these events on a timeline from left to right, and you do this as a first step. Once you have your events, you then ask yourself, "What triggers this event, or more importantly, when this event occurred, what actions do we need to take?" Then we might have commands. Then you start putting up commands next to your events. In this case, the aircraft type was changed, might need to rebook, and so that publishes an event, and when that booked flight was changed, we need to notify the customer, and we might have to wait few days, whatever the rule is and then publish events.
The thing about these commands is, because we know the commands can fail and we can ask ourselves, "What do we need to do if we can't rebook the flight?" That question is not something we as architects or programmers can answer. That is something that the business knows or the domain expert knows. They are the ones with the constraints, we have to ask the right questions, and knowing that these commands can fail will help us ask these constraints questions and try to model that in our code, so we come up with this.
Once we have this model, writing code is much simpler. You have an event, you're going to have a handler that needs to get invoked when that event happens. In this case, when the aircraft type was changed, we need to find all the relevant routes and stuff and we need to rebook the flight, so we send a message to rebook the flight, and inside the rebooking thing, we look at the routes and come up with whatever rebooking is, and then we publish an event saying, "This thing was rebooked." Now we can have the saga thing I talked about where you're going to have multiple messages, you need to keep track of how much time passed and if the booking was canceled or whether the cancellation period elapsed. For that, you have a saga that implements and does whatever it needs to do. Writing code is much simpler once you know and model like these things along with your domain experts.
Naming Things Is Hard
As we all know, naming things is hard, and it's one of the hard problems of computer science. When we name these events, we are so accustomed to crud-type things, we tend to call customer was created, customer was updated, customer was deleted. If you just think back for a second, who deletes customers? "You are deleted," we don't do that. We might say the customer was deactivated or whatever the right term is, but we don't delete customers. Naming is hard, and it's much easier when somebody comes up with the name, and you look at the name and go, "Yes, that makes sense, and that's a much better name." Coming up with names is hard. One thing that I used to do before is, we're using events and we're using the past tense to name these events, so we want to say a PDF document was generated. Yes, it conforms to how events should be named. It's in the past tense, it's a thing, its significance important has happened.
What is PDF document generated mean to the domain expert? That's just a purely technical term for us. That doesn't mean anything to the business domain expert. It might be that the letter was shipped, or certified mail was sent or something that has a very significant meaning in the domain. You want to try to capture those names and that language in your code. Yes, naming is hard, and this is where the collaboration and the communication that you have with your business people and the domain experts are important.
Let's go back and take a re-look at the requirement. The requirement was passenger gets notified with a new booking proposal. The word was new booking proposal, and yet, I had originally called it rebook flight. We're not rebooking flight, we are just dealing with proposals, we're trying to propose a booking. Again, it wasn't the booked flight was changed, it was that rebooking was proposed. You take this knowledge that you learned from your domain experts and go and modify your existing models. That is something that you're going to have to do. These are things that you're going to learn every day, and that's ok. You need to take that knowledge and apply it towards your model. That's how your code is going to use the same language that the domain users use, and you get all the benefits of domain-driven design of trying to write software that aligns with your business.
It doesn't happen like right away. I think it was George Box who said, "All models are wrong, but some are useful." When we first design something, it's going to be not perfect. It's not going to be correct even sometimes, and that's ok. That's completely ok. We just need to take our knowledge of what we learn and try to apply to our models.
We need to have a very healthy obsession with language, because once you start using the language of the domain, then everybody can understand your code. If you leave and another person comes in, and that's fine. They can still communicate with the domain experts.
The thing that I used to do was, I used to name my handlers as whatever the event name was, in this case, aircraft type was changed, I would call my event handler AircraftTypeWasChangedHandler. Well, handler is just an implementation detail, you don't need that. Aircraft type was changed, that doesn't tell you anything about what's going on in the handler. Take a look at what your code is actually doing when that event has been received, and call it the appropriate name.
In this case, that handler was trying to propose a new rebooking. Yes, you can have sometimes long class names, but it's readable, it's much more understandable. Ditch the handler, but use the right domain language in your class names, in your handler code, everywhere.
The same thing with the saga name. It wasn't a booking change policy, that saga was clearly dealing with just the grace period, "What should I do if the customer doesn't cancel within a certain period of time?" Give your handlers and sagas proper names. Question yourself, "Is this right?" in your code reviews and peer reviews that you do, have somebody check for language and do these things.
The other thing is messages are immutable. When you use some things like ReSharper, it's going to generate properties with gets and sets. If messages are immutable, they don't change. You can't change an event. These are schemas published by one bounded context. The other bounded context that receives it, it can't change it. Why should we have setters when we try to describe the schema messages? You don't need setters, so you can get rid of them.
Also, look for some concepts in your domain that are immutable. For example, maybe you're dealing with accounting, and that's your domain, and in this accounting, you can only add stuff, you can't go and update it. If that's the thing in your domain, don't let your code go and make it updatable. Make that domain decision whatever that was immutable, the business domain expert said, "No, you cannot do this," take that concept and then make your code in such a way that you can't change that aspect inside your code.
Being Aligned with the Business
One of the other important things about domain-driven design is this whole notion of being aligned with the business. If we want to modify this requirement and say that, "We don't want to really send notification to every economy class passenger. We only want to do this for platinum or first-class passengers," because when an aircraft type is changed, that's who it matters most because some people, based on the aircraft type might lose flat business seats and go to angled business seats, and they might have paid a lot of money. We want to make sure we keep them happy. We don't want to do this for everybody.
Let’s say you have a requirement like that. Now, you're in a little dilemma because you have the booking context, and it's trying to work with the aircraft type was changed, but it needs data from the loyalty context, which knows whether the customer is preferred or platinum. The booking context doesn't have that data. How does the booking context go and get those data from loyalty?
Temporal Coupling and Autonomous Decisions
In order to fulfill this business requirement, we need to make sure we get this data, and once we get the loyalty status, then this booking thing can make a decision on whether needs to send notification. The trouble with this is, yes, you can call rest calls, or you can have synchronous communication, but what happens here is, if whatever reason loyalty context isn't available, you are stuck. You can't proceed. You have resources that are stuck and waiting in the booking context that cannot make decisions unless you have the data. How do you get rid of this dependency because this is the thing that is going to prevent you from being reliable and scalable?
Temporal coupling is where you have a dependency on time where one service or one component cannot complete its operation until the other party is done with work. In order to get rid of this temporal coupling, what you can do is you can use events. You have the two contexts, the loyalty. The loyalty context can publish events whenever a customer was like promoted to gold or platinum. Booking context can simply receive these events, and when it's receiving the events, can store it into its own database, "This customer is gold," or "This customer is platinum." When the flight planning context is publishing this aircraft type has changed event, then for the proposing rebooking component, it can simply look at the data that it has to make that decision. It doesn't have to go back to the loyalty.
The advantage with this model is that flight planning and loyalty and booking, they can all be scaled differently, and they don't have to be up all the time to make these decisions. However, the question is, what if this customer was promoted to gold, event happened, and inside the promotion information, we have that customer, but maybe the customer was de-promoted or lost the status, and in this context, you have them as platinum customers? What happens when there's this data staleness? We can't answer that question, but we can ask the domain experts, "How do we want to handle that case?" It might be ok for a day or so, and that might be completely acceptable with the business. We as programmers and architects and designers can make that choice. However, by making this explicit in our design, we can ask the right questions and therefore, we can choose the right answers. A lot of the times, when we have race conditions or requirements that look like race conditions, it really means that there is something in the domain that we haven't really teased out. There's a policy or requirement, and we just haven't had the right answers, so we just have to keep digging for those informations. This design makes that very explicit.
Deployments
In this model, deployments become easier. When you have a version 1.0, and you want to upgrade it to a version 1.1, you don't even have to stop it. You could just bring up a new module and see how this works. If everything works, you can stop the other service or other component. This also gives you a nice way where if the business wants to say, "Ok. We want to try out a new policy. We want to try this only for platinum customers or something where the behavior is different," you could try it and let business know how things are working. If that's how they want to proceed, then they can stop the old behaviors. You can have two behaviors running at the same time and evaluate on how things go and take decisions. You are giving more flexibility to the business to do things like this.
Of course, you can always stop once the business is ready or once you're happy with your deployment, you can stop it in one. We all know that sometimes deployments don't go so well. What happens if this fails? The good thing with working with events and messages is that when this message goes to your second version 1.1 and it fails, it isn't lost. It's transferred to this error queue. What happens is now you can take a look and see why it failed. A lot of the queuing systems have this ability to poison messages because you can have some kind of transient error handling built-in where for some operations like database deadlocks or some things, the exception will tell you, "Please retry this operation." In those cases, a temporary or a transient retried, a quick retry, would work.
In some cases, it doesn't matter if you have an old reference exception or something in your code, if it doesn't work the 5th time, it's not going to work the 100th time. The queuing mechanisms have this way, so you're not trying to DoS your system. They take that message and poison it. That message gets moved to the error queue. Now you can debug and then figure out. In the meanwhile, you can stop this bad endpoint, and all of the messages will flow to your original endpoint, and then you can even take the messages that are in this error queue and play it back to the original endpoints, so all of your stuff gets processed.
Monitoring
This thing gives you reliability and also scalability, because now if you have a lot of load and if you want to bring up five instances, you can do that. In a system like this, monitoring becomes very important. When you have broken down your system into small pieces, it's not like where you had this big monolith where when something fails, everything is going to stop, everybody's going to know, from the database admin to everyone. That was, I suppose, one good thing of having a monolith, you know when things failed immediately.
In a system like this where you've taken the time to design small parts, you can have a really small part failing where everything else is working. Messages might be getting piled up in this queue. If you're not looking, you don't want to get a call from a customer who says like, "What happened to this thing?" Or you might have an SLA where you're supposed to fulfill whatever the operation is within a certain time. Monitoring becomes super important. You can look out for different metrics like queue length, how much time are you taking to process each message. There's a lot of things that you can look out for that you need to monitor.
Takeaways
If you want to take away a few things from this talk, first and foremost, I think the communication, the collaboration that you have with the domain experts is key because that's where all of the good stuff comes in. We want to build the right thing, we don't want to build any software, but we want to build the right thing. By event storming and having this collaborative way of talking to people, we can ensure that, and also recognizing that our models are not perfect. That's ok, we need to just evolve. We need to take the understanding from the domain and apply it. Some of the information is going to be like a hallway conversation, and that's ok. You are going to get some information from that. You might find out that the event that you called was not a right name. You take that information and you go refactor. It's all about refactoring with an obsession for domain language.
Our whole idea here is, we want to try and model the behavior of the business. That's where all the complexity lies. By having conversations and remodel this behavior, we're in a much better shape. Use events as a way to communicate between bounded contexts. Messaging as a technology gives you a lot of this freedom and autonomy.
If you are writing really small services that are autonomous, that can scale on its own, to me, you are in the land of microservices. When you combine the discipline of domain-driven design and then use a technology like messaging to communicate between bounded contexts, you naturally land up in a land of microservices.
If you wanted to learn more about DDD, there's Eric Evans' book, the blue book. If you start, I suggest you start with part four, strategic design. I think Eric himself mentioned that if he were to rewrite the book, I think he would have started with part four, so it might be a good idea to start with part four. There are two DDD very specific conferences. One's in Denver in September and the other one is in Europe in Amsterdam. I think they're event sourcing days, beginning DDD or foundation days, and so on. Both these conferences are fantastic places to go to learn more about DDD, and also there's an ebook that's available for free, published by DDD Europe. It's on leanpub.com/dddfirst15years. It's been 15 years since Eric wrote the DDD book. These are all great resources.
See more presentations with transcripts