Better software design with domain modeling

I spoke at Øredev 2023 about how to use domain modeling to improve software design. I explain three lenses. The video loses a few of the slides for some reason, but the slides are available for download below.

Download slides

Transcript

Hello. My name is Eric Normand, and I'm going to be talking about software design and domain modeling. I'm writing a book on domain modeling, and so I'm very excited to talk about some of the ideas that I'm going to be putting in the book. So domain modeling is a subset of software design, and software design is really subtle. It's a very hard, multi-dimensional problem, and I don't think it can be broken down into principles and rules of thumb like we typically do when we talk about software design. Software design, like any kind of design, is about making good decisions. There's a lot of decisions we have to make when we're writing software from little things like whether to use a for loop or a while loop to bigger things like what kind of data structure do we use when we're implementing an algorithm all the way to the architecture of our system. And to make those kinds of decisions, thousands, millions of decisions in this big, multi-dimensional space, we need context, we need information, more information, so that we can make better decisions. And where does that information come from? Well, the best source is our domain. So if there's, well, sorry, if there's one summary slide, it's this, that we need good information to make good decisions to do good design. And because we can't rely on rules of thumb and principles, I've organized my book and the material in this talk into what I'm calling lenses. So each lens gives us just a little bit like a slice, a cross-section through this multi-dimensional space, and it gives us just another peek at our domain. And if we get enough slices from different angles, then hopefully we can put together a complete picture of what our software should look like. Hope that metaphor lands. I added that yesterday. So I have ten lenses in my book, but I don't have time to go over all of them. Today I'm only going to do three and really only go briefly into them. There's a lot more than I just won't have time for. You'll just have to buy the book. All right, so let's talk about data. And data is like kind of at the base of most modeling that we do. And it's kind of the kind of thing that you learn really quickly when you're a programmer. So after a couple of years, you learn how I know how to model this in some JSON. I don't need a lot of help. I don't need a lot of guidelines. But what I'd like to do is explore the process that we go through a little bit deeply, because you already understand how to do it. We should go deep and really see what is actually happening in our thought processes while we do it. I hope this is basic, but also deep, like a deep view. We've got a Starbucks-style coffee shop. Show of hands who understands when I say that. That's like obvious. Okay, good, good, good. You never know in a foreign country. All right, so there's three sizes of coffee, super, mega, and galactic. There are three roasts, and there are a bunch of add-ins that you can add, like syrups and flavors and soy milk and stuff like that. Simple model, just for as an example. Now, if you ask me, how would you represent this in JSON? You want to have a coffee order as a piece of data? I would just do something like this. Well, I'd say, you know, it has a size, and there's a string that represents the size, and it has a roast string for that. And then we would have an array of strings for the add-ins. All right, again, show of hands who thinks this is like super obvious. This is not the only way to do it, but this is in one way that you would do it. Oh, great, great, great. So, but what did we actually do when we did this? Because this, we're all experienced developers here. We kind of do this automatically all day long. Let's go deeper into it. So, the sizes have a relationship between each other. The three different sizes, we have to choose one of them, not zero, not more than one. We always choose one. And I'm going to call that an alternative. So we choose one out of the three alternatives. Then we take each of the sizes and we give them a string that makes it human readable. We could give them numbers, but then it's not so easy to correspond them. And then we make sure that those strings also have the same structure of relationship between them. So we have to choose one of those strings. And we can represent that as a TypeScript type. If you're not familiar with TypeScript, this basically just means we're defining a new type called size and it's a union of those three strings. It has to be one of those three values. All right, we can do the same with the roasts. These look very similar. It's an alternative. We have to choose one roast. We'll give each one a string to represent it. Again, you could represent it with something else, but we're doing strings right now. And then we make sure that they have the same relationship, this one of relationship. Add-ins are a little more complicated. There's going to be two different levels to it because we can have more than one. So we'll have a first level where we just name all of the add-ins. So we name them as an alternative. And so we get something similar to what we had before. But then there's this other layer where we can say we have zero or more of them. And we'll call that a collection. And then we can make a type. This was just an array of add-ins. That's what the TypeScript looks like. Now we have the coffee itself. If we have this coffee, it's a super burnt with soy milk and espresso shot. We need all of these. This is the relationship between these things. We need all these to make a full complete coffee. So we're going to call that a combination. A combination of size, roast and the add-ins. We know there are types. And so we make this what's called in TypeScript an object type where you have the size and the roast and the add-ins. Okay. So here's the size again. I'm going to draw this line. Above the line is the domain. This is stuff that we looked out into the world. We saw how the coffee shop was being run. We talked to people. And this was, this was it. This was what was happening. And below that is our model. This was our coding that we did. Above it we analyzed a relationship out of the, out of the domain. And then we encoded it. Okay. So I'm just trying to define these terms in context because we'll be using them a lot. Now I'm going to take that line and move it up so I have some room to draw stuff. Because we had choices. We used a union type of strings to represent size. But that's not the only thing we could do. So we had that union type of strings. But we have this relationship, this alternative relationship. We could also use a string and enum. TypeScript also gives us that. It looks like that. Slightly different. Now we could also use classes and interfaces. So we make an interface called size as a name. And then we have three implementations of size, different classes that have the names hard coded in. Now how do we choose between size? This is one of our design decisions we have to make. One thing about software design that I'm trying to emphasize is that better designers evaluate more options. They see design decision points, decision points where they can open up different spaces and see that we have a choice here. We don't have to do it in the obvious way. Just like a chess master might evaluate more moves than a beginner. A good designer is evaluating more options. How do we choose between these? So one thing about the data lens is that there's a concept that I'm calling fit to way of evaluating the different options you have for encoding. So let's look at what it looks like. So if we have the size and let's ignore add-ins for a minute and just see size in the roast, we have this combination of the two. So every coffee is going to have a size and roast. So how many coffees are possible with these three times three? Well it's nine. It's pretty obvious. Okay. So there's nine possible states our coffee order can be in. Here's our combination without the add-ins in our encoding. That was from the domain. We counted the number of coffees we had. Let's look at our model and see how many this object type can have. Well it can have a super and raw and we can just go through, I don't have to read them all, but there's nine. All right. So it's the same number as in the domain. So our model has the same number of states as our domain. Domain and model. What's the overlap? There's nine. And there's none in the model that you can't represent in the, that don't mean anything in the domain. And there's none in the domain that doesn't mean anything in the model. So this is what I'm calling perfect fit. Your model fits perfectly with your analysis of the domain. Let's look at add-ins which just give it away. Is it going to be a perfect fit? So we have these five add-ins and we can have zero or more of them. How do we count them? How do we know how many possible combinations there are? It's actually kind of complicated. So we're just going to look up to three because if you could have like a hundred add-ins, it starts to get like factorial big number of combinations. Okay. But this is just take a look. So for zero add-ins, it's an empty array. That's easy. That's one case, one state. As we add an add-in, we see it's just going to be one add-in string in that array. So there'll be five, right? So now we're up to six. For two add-ins, we have to have two strings and you can do the math or you can just say wait a second. I already see a problem here because I can say I want a coffee with soy and espresso or I want a coffee with espresso and soy. Are those really different coffees? They're going to taste the same. They might even be made the same way in the same order. And so maybe now we're starting to see a problem with our encoding. And the problem only gets worse when you have more add-ins. You have more possible orders that mean the same thing. So we didn't even have to count them and we already saw problems with our encoding. So again, back to this Venn diagram to see where the problems are. We can represent everything. We know that this model can represent everything in our domain. We can feel that intuitively. And there's zero, so that means a zero that are not representable. But we have these meaningless distinctions. We have states that are equivalent, but they have different encodings. They're equivalent in the domain, but in our encodings, they look different. So we can't even compare for equality anymore to know if we have the same coffees. And that's a problem. It's less than perfect fit. It's a solvable problem, but it's a thing that should tickle our senses of maybe there's a better way. So again, here's our model. And I'm going to do the same kind of thing where I push up that line. We're talking about collection right now. And we'll just look at all the options we have for encoding a collection. So we have the array. This is what we had before. We could use a set, but we know that we probably want multiple of the same add-ins in our thing. So it's not going to work because we need duplicates. And sets don't allow duplicates. We also have a thing, if we're using JavaScript, TypeScript, we have an object. And that could be interesting, where we have the add-ins name is the key. And then the number of that particular add-in is the value. And so it looks something like this, right? And in an object, the order of the keys is not important. So we won't have that problem of having, you know, almond and soy and soy and almond. The only thing to keep in mind is that this requires us, because TypeScript doesn't let you -- you see the type of that add-in key is string. It doesn't let you use size as a type of that key, even though it is a string, just, you know, a subset of string type. It doesn't let you do it. So we'll have to keep in mind that maybe we'll need a run-time check or something. Just a thing to add to our -- the complexity of our space. Okay, just a show of hands. Who, at this point right here, who thinks array is better than object for representing add-ins? One person? Okay. Who thinks object is better than array? Oh, okay. Interesting. And who thinks set is better? Yeah. Some people didn't raise their hand. Is there another option that I didn't have on there? Maybe the other option is Twitter or something. Okay. Map? Okay. All right. Cool. So let's just compare array and object. Again, this is just us going through and trying to get more information from our domain. Also from our encoding, we can use our encoding to just dig in a little bit deeper and see if there's more we can learn from it. Possible too. Possible too instead of strings. Yeah. Yeah. Okay. So both of them can represent no add-ins. That's easy. If we want to represent just an espresso shot, that's also clear, easy in both. Here we see when you have two espresso shots, what it looks like. This one is two espresso and a soy. Now again, we we see that we there are multiple ways of representing that array of three different three different add-ins. We could make a mental note that we could sort this array so that we could compare it for equality after so we could have some kind of normalization function. But oh, sorry. Press the wrong button. Okay. If we have two espresso's, we do have a problem in the object as well, which is we said it's a number. We have no way of restricting that it's a natural number. You could have a zero or even a negative number in there or even in JavaScript, there's no way to say it's not a float either. So that could be a problem. And so we might have a normalization function that like removes zeros and negative numbers just so that we can compare because espresso two soy zero should be the same as espresso two. Okay. So at this point, I don't know what's better. I can't tell between them. I like objects, but I think we could do better and find some more information. And we're going to have to use other lenses to get that more information because that's kind of the end of the data lens. And we're going to do that in a minute. But first, we're going to talk a little bit more while we got it all loaded in the cache. We're going to talk about a thing that we didn't kind of glanced over, which is that we moved the line up, but we never moved it down. And we have a lot of choices about how we represent or how we see coffee in our coffee shop. This was the choice that I presented. But you could imagine a different coffee shop that sold it by milliliter instead of three sizes of cups. They sold, you know, if you buy it commercially, you might be buying it by the volume. And so that would be a count, right? And in a more traditional coffee shop, the size of the cup is more related to the style of coffee that you're making. So an espresso is like this big, an Americano is in this other cup and a cappuccino is like this. And so it has this interesting relationship to style that is not present in a Starbucks, right? And in a Starbucks, there's like, you can get a cappuccino small, medium, large, like they're all the same cups. I just want to bring this up because we are dealing with a very complex problem here that you're not just modeling a domain that's given to you. Often as programmers, the business side who's giving us the requirements, they don't have a clear answer of what their requirements are, what their domain answers are, how to analyze the domain. And we're often the ones to have to go do that for them. And so this is part of software design too. This makes it even more multidimensional because we could encode it the first way and then tell the business, hey, it seems like maybe, maybe after I encode it, I realize you might want to do more of a millimeter style instead of small, medium, large. It's an example. Probably have better examples in your own experience. But we have, now we can talk about kind of the process of domain modeling. So we get stuff from the domain, we analyze it and we get this idea of the relationships in our domain. Then we encode those into a model. I like to use code, like types or even schemas, whatever you want to use in classes, whatever you have in your language. We encode that into the model and then we analyze it for fit. Does this really represent what I've analyzed out of the domain? And we can go through that loop on the right many times where we try different options for how to encode it and see how they work. But we can also go back to the domain and take a new look that maybe, maybe the way I saw it the first time is not the right way. It's not the optimal way. So we can go through both of these loops and go many times around and with fast feedback. Okay. So that was the data lens. We're going to look at the operations lens now. Because operations help us constrain the data. We're not just looking at how do we write the data down. We also have to look at what are we going to do with that data. What are the use cases that we need to support? So this is a rendition of the touch screen that a cashier would have at the coffee shop. They're going to be able to select the size. And currently, mega is selected. It's highlighted. They can select the roast. And then they can also like click plus and minus to add and remove the add-ins. So we can squint and kind of see already this domain of operations that we're looking at and we can make function signatures for them. So one function will be set size. It takes a coffee and a size and it returns a new coffee. I'm dispensing with the TypeScript because it gets really long when you're trying to write the types of everything. We'll just have to imagine based on the names of the arguments what the types are. It shouldn't be hard. Okay. So we have set size. We have set roast. We have add add-in and remove add-in. I figure everyone can do this. Given that interface, break it down into use cases. The cashier is going to have to be able to do these things. Now I'm using also a functional style. I'm a functional programmer. So instead of like a method on an object, I'm taking an argument and returning a copy that's been modified. Show of hands if that makes sense to you. Awesome. Awesome. Okay. I like using function signatures because they're good at representing use cases. You got your inputs. What stuff do we need to know? And then you got your outputs. What is going to be the result when I do this operation? They're also good because they're code. And so you're one step closer to actually having an implementation. But we're leaving the body out. So let's take a look at the function signature and what's in it. It's three things. It's the name. It's the arguments and their types. And it's the return type. And notice we don't have the body. We're not implementing it yet. This is a little token that we can manipulate because we might come up with a set of operations and then decide no, those aren't the right ones. Those aren't the ones we want. And so we don't want to waste all the time implementing them. We just want to be able to talk about them and reason about them. They give you a very concise and precise definition of the requirement. And you can use methods. I like functions. If you like the OO stuff, that's great too. It's the right level of abstraction because it corresponds to the use case but it's also code and precise. And then we can always implement it later. This is actually a very easy function to implement. And so why implement it now? We should just be thinking and talking. Okay, but it has -- there are design decisions here. There are design decisions here. What happens if we have a size that's already been set? So it's already mega and we ask it again to set the size to mega. I know what I would say immediately without thinking. I'm wondering if you would say the same thing. What I would say is, well, just return the coffee unchanged. What's the big deal, right? But this is a choice. There are some languages where that's an error. They would throw an exception here. There are some languages like Python, like popular language, where if you try to get a key and a key does not exist in the dictionary, it will throw an error. And to me, that's weird. Like why would you want to do that? But it's a choice. It's a choice we have to throw an error. This is one of the only recommendations I'm going to give in the talk instead of just asking you to dig up more information for yourselves to make decisions. I think this is a really bad idea. And the reason is, it's very hard to build on top of this. It's very hard to compose operations when they're throwing errors. You never know what someone's going to pass to it. You want to have what's called a total function. So total functions are functions that are valid for -- they give you a valid answer for all valid inputs, all valid arguments. So one option we have -- we have three options, right? There's three parts to our function signature. One option is just to augment the return type. So we can use the TypeScript notation, the coffee or null. So in case we don't change anything, we're going to tell the caller, I didn't change anything. So here's null. You could use a false or something like that just to indicate nothing happened. There's a no op. And that's a choice. We could do that. Another choice that we have is to restrict the argument types. So we have this restriction that says that coffee.size can't be equal to the size. And some type systems let you do that, which is pretty cool. So you can even have a compiler error if you violated that. Some you would have to put in like a precondition. Restricting the argument types. And then the third thing we could do is modify the meaning of this function. So you can squint and say like set size, if it's not setting it, it shouldn't do anything. It should -- it should be an error. If it's already set, you're not setting it. But we can also squint and say, well, what we mean by set size is like make sure it's this. Make sure it's a mega. Even if it's already is, that's fine. But if it's something else, make sure it's a mega. And so we could return this unchanged. That's kind of changing the meaning of this function. All right. Just a recap of total functions. There's three ways to handle, to make a function total, that if it's not total, you augment the return. So you incorporate that what would be an error as part of the return. You can restrict the argument so that you never hit that error. Or you can change the meaning so that it's not an error anymore. But also a recommendation, use total functions. They're good. All right. So we looked at the POS. The POS is used by the cashier. But we haven't talked to everybody else. It's really good idea when you have -- when you're looking at operations, to see as many operations as you can. To get a complete -- as complete as you can, set of operations. Because that gives you more information. It helps you make better decisions. What happens is we often like implement the easy stuff first. So we write all this code for the getters and the setters. Those are all easy and obvious to write. Even your IDE can generate it for you. And then when we get to the complicated ones, we're like, oh, it's not going to be easy anymore. And we wish we had changed the data structure. If you get a complete set first, you can actually find the ones that are more complicated first and learn that earlier. Learn before you've implemented everything and chosen a data structure. You can look at the complicated operations and learn that you might need to change your data structure. Okay. So we talk to the barista and they say, when I'm looking at this array of add-ins, it says soy espresso soy. I'm looking at it as a list and I'm saying, oh, soy add the soy, put it back in the fridge. Then I go walk over to the espresso machine. I pull an espresso, put it in there. And then I see it says soy again. I got to go walk back to the fridge, open it up, shake it again, pour it, put it back in the fridge, and then I serve it. Why didn't you just tell me there were two when I started? Right? So we can translate that into a use case of they need to know how many add-ins there are for a particular type of add-in. So even if it's in an array, we want to say, there's three soy shots in this. Marketing, we go, and they say, look, I don't care how many hazelnut shots there are in a particular coffee. I just want to know how many coffees in general have hazelnut in them, because maybe we'll remove it from the menu if no one's buying it. And so we translate that into, well, we need to be able at some point to say what coffee, does this have hazelnut in it at all? So it's a Boolean, yes or no. And then we talk to the cashier, and they say, well, I'm going to have to tell the customer the price. So can you calculate the price for me? And this becomes, you know, return a number from the coffee. I'm trying to keep it simple here. Just go through these three. You might have hundreds of these. Okay, now with these operations, we're going to implement them in our heads. I'm going to write it down on the slide so that we can all see what's in my head. But for an array, we're going to implement how many add-ins. We can clearly see, like, well, one way to do it, we're going to have to count, like, the soy strings in an array. We can do this filter with this lambda function we pass in, then take the length. But notice it's a linear search. Kind of all we have to see at this point. Linear search, basically kind of hard for the computer to do. Object, when we implement it, it's a constant time operation. We look up the add-in, and then we have to order it with zero because it might return undefined if it's not in there. That way we always get a number. Constant time. Okay, it's like a point for object. If we do the other operation has add-in, we have to do, again, a linear search. We have to look through the array. In the other case, where we use an object, we do this double-not trick to turn it into a Boolean, but it's constant time also. What I'm trying to do is show how we can use these operations to help us get more information about our data model. Even though we're not in the data lens anymore, we're helping constrain it. Show of hands, who is in favor of arrays at this point? For add-ins, nobody. Who's in favor of objects? Okay, just about everybody. Just to reiterate the model, we're looking at our domain. In this case, we analyzed out the operations and the use cases. We encoded them as function signatures. Then we constrained what we had before using this lens. We could also take a new look. We could go back and ask for more operations, that kind of thing. Maybe tell them, "Hey, this operation won't be possible. Is there something else that'll work?" Okay, let's go for the third lens of the day. This is volatility. I have a lot of things I could rant on about volatility and the way we design software. But I won't. I'm going to spare you that. If you want me to rant, just come find me afterwards. Volatility is basically that things change, that our domain changes, our software has to change with it. We have a fear of change, I think, in our industry. Okay, but one thing we can do is we can go to the business people and we say, "How often does this size change? Am I going to have to be changing this code all the time? What's going on?" We can get more information about the domain. In this case, the business users know. They've worked there a long time. It's been like not. They haven't had software yet, but they've been running the coffee shop for 10, 15 years. In this case, this scenario, they say, "We've never changed the sizes." We opened with these three sizes and they're still the same after a decade. We don't have any plans for changing them. We say, "Great, thank you for the information." Same thing for the roasts. We ask them, they say, "We haven't changed them." Very lucky. Then we ask them, "What about the add-ins?" They say, "Well, we do have these seasonal add-ins, like for fall, we do pumpkin spice. Do they have pumpkin spice here? Has it made it here?" It's like a huge thing in the US now. You can get everything pumpkin spice. Then people miss it because they're like, "Ah, I was out of town during the fall and I missed pumpkin spice. Can I get it?" You can still get it. Then they have peppermint for winter, stuff like that. Then we go talk to the manager of the store and they say, "Well, you know, we actually sometimes run out of hazelnut syrup and we want to take it off the menu and we won't get it until the next day." It has to be like from one minute, one hour to the next, we have to drop this thing. Sometimes we want to try a new flavor and so we bring it in. Now, this is going to be a lot harder. We can see that we're going to have to facilitate this change within the business. We can lay out this spectrum on the left-hand side, things change in terms of decades, on the right-hand side, things change hourly. You can imagine the stuff that changes hourly, you're not going to want to get a Jira ticket to say, "Remove hazelnut syrup from the production deploy because we don't want to sell that anymore." It's too fast for the development cycle. But at the same time, if something doesn't change for decades, maybe they change their business model and they have a fourth size. It's okay. You hard-coded it in your code. You can do a deploy once every 10 years to add a new size. It's not a big deal. We're just going to bet that we'll never have to change it. Then if we do, we just change some code. No problem. We can actually lay out these things on this spectrum. Here, I put the size and the roast in the decades. In the hourly, I put the add-ins. Then I put this other one in the middle, the sale. I didn't talk about that. The marketing team has sales and those change pretty frequently. The sales can include coupons and other stuff. Those are in the middle. I just wanted an example that goes in the middle. It's a continuous spectrum and it's really hard to work with when you're trying to make decisions. I tend to bucket them like this into three. The stuff that changes the lease frequently, we can use a closed encoding. What closed means is from the open closed principle, closed to modification. Closed to modification. You can modify it, but you have to change the code. It's called closed, but it just means you have to change the code. You have to go in, you add a new size, and now your type is different, so all your functions that use that type, maybe they have to change. You just have to go follow it. If you have a good type system, you'll get good error messages. Otherwise, you'll have to run a lot of tests, figure it out. In the middle, we have a thing called open. The marketing team wants to add a new kind of sale. They don't want all the other stuff to break, and so they want something that lets you add new code without modifying existing code. Finally, we have runtime. This is where you just store it as a value and you can put it in the database. Changing it doesn't have any -- the code should not have to change. You just have new values in the database, so you remove values from the database. An example of the closed that requires change with the size, if we use this union type, if we add a fourth option or remove one of the options, we would probably have to review all of the functions that use size. Just to make sure that we weren't using one of those sizes that doesn't exist anymore or that we're handling all the cases, like the new case. To add new code, one thing that TypeScript gives you is interfaces with implemented classes, implementing them. Notice if you're familiar with even an other object-oriented language like Java or Ruby, it's the same kind of thing. You just add a class, and as long as you're implementing the same interface, and in this case it's easy. You just have a string as the name, the existing code should work with that new class, with new instances of that class. And then runtime, we can change our type instead of being a union because we need to be able to change it more often. We just say it's a string, and we have to do runtime checks or that the strings have to be in a table in the database, and we can mark them as available, not available. Whatever query we're doing on the database, we'll have to take that into account. We can also augment it. It's not limited to string. We could say, "Well, we're going to have other data along with the name, such as the price." So this helps us figure out whether our add-ins are correct. And it's another way of triangulating our decisions with just more information. So I would say for add-ins, we wouldn't use the type, the union type, that I had before of strings. I would just make it string and have to do everything at runtime, just because now we're going to have an interface, part of our domain, an interface for adding and removing add-ins so that the store managers can do that. Okay. We're at the review. We're at the conclusion. How am I doing on time? Okay. So we looked at three things. We looked at data modeling. We're analyzing relationships out of the domain and then encoding them. And then we can use the domain, compare it based on the number of states that we can do. We can evaluate the fit. We also looked at operation modeling. We looked at function signatures, which I think are an unused -- I prefer function signatures to UML use case diagrams. I think that they're more precise and they actually get you a step closer to an implementation. We learned about total functions, so don't have error cases. Don't throw. This isn't always -- well, the no throwing is always possible, but no error cases isn't always possible. Sometimes, if you have a web request, there's always a time out possible. But you can incorporate that error into the return value, augment the return value. And then we looked at evaluating the complexity, not just the big O complexity, but also like is it really hairy to implement? Is this hard? Does this require this complicated algorithm? Maybe there's a data structure that makes it easier to implement. And then we looked at the volatility where we're analyzing the frequency of change and bucketing into closed open and run time. Okay. That's my talk. You can -- if you're interested in following this, you can get on my newsletter or find my podcast at erignorman.me. I've got a lot of the ideas as I'm working them out. I've been working on this for like two years already, and I hope it's -- it feels like it's getting there. It feels like it's -- like the ideas are fleshed out and they make sense, which is not the case before. They're getting there. So if you like to follow along in the journey, you can join there. Also, that's where I'll announce the book when it's ready. Yeah, and I used an image, so I got to -- got to give a link to it. All right. Any questions? Yes? >> Can you share those three lenses as -- what are -- can you find them all -- >> I find them all valuable, but I'm -- I focused on these because the -- I'll repeat the question. Like why these three lenses out of the ten that I chose, the data modeling one, I feel like is not appreciated. People jump directly. Like it's very common in what I -- what I was taught in OO circles to just immediately make an interface with classes, and I wanted to show that we do have choices there and that we need to take a step back and expand those choices if we really want to do good design. The operations one, I think people don't think enough about the complicated operations. They write the getters and setters immediately, and in the volatility, I guess I'll rant a little bit. I feel like agile, you know, agile is a multifaceted thing, but a lot of the -- a lot of the damage that has been done by agile is to say like change is ever-present, unpredictable, and maximal. Like software is just going to change, requirements are going to change, business people just keep changing their minds, and we -- like there's nothing we can do about it except put in direction everywhere. Why design it because it's going to change, you know, those kinds of things. And I wanted to show that you can look and see in the domain does it actually change and make good choices based on that. You don't have to put in direction everywhere and make everything changeable all the time. So that's why I thought it was an important idea. So it wasn't about -- I tried not to make it about me. I tried to make it about what I thought people needed. So, yeah. Thank you. Any other -- oh.