Talks
Build Complex Domains in Rails
Summarized using AI

Build Complex Domains in Rails

by Mike AbiEzzi

In the video titled "Build Complex Domains in Rails," Mike AbiEzzi, a software consultant at Quick Left, discusses strategies for effectively modeling complex domains using Domain-Driven Design (DDD) principles within Ruby on Rails applications. The presentation highlights the challenges faced when scaling Rails applications, particularly when the complexity and the team size increase. AbiEzzi emphasizes the importance of clear communication and a shared understanding of the domain among team members, which is essential for creating maintainable software.

Key points covered in the video include:
- Understanding Domain-Driven Design: DDD focuses on creating a model that accurately represents the business domain, facilitating better communication between developers and domain experts.
- Defining the Domain: The video uses the iOS App Store as a hypothetical domain, illustrating how to refine the terminology and understand various elements such as apps, sellers (developers and companies), releases, and reviews.
- Establishing Ubiquitous Language: It’s crucial for all team members to use consistent terminology to avoid confusion and ensure clarity in development and communication.
- Relationships in the Domain: AbiEzzi discusses the importance of identifying and simplifying relationships between models, ensuring only necessary connections are maintained.
- Aggregates: These are crucial in managing complexity, as they encapsulate related entities and control their behavior. For example, the video outlines how to manage app releases through a method called 'submit_release', demonstrating how aggregates function in practice.
- Data Access and Scopes: Using Rails scopes to give names to specific queries clarifies intent and expresses domain behaviors, leading to better practices in querying data.
- Value Objects: Differentiating between entities and value objects helps in managing domain complexity, with value objects being immutable and simplifying designs.
- Domain Services: These are necessary for handling operations involving multiple aggregates, providing a mechanism for complex processes to occur without tightly coupling components.

In conclusion, the talk emphasizes refining your domain iteratively, ensuring sound communication, and strategically applying DDD principles to streamline your Rails applications. The benefit of having a clear structure allows the application to grow sustainably while minimizing defects related to business logic. Overall, the presentation serves as a practical guide for developers aiming to apply DDD in their Rails applications, providing actionable insights into managing complexity in software development.

00:00:26.800 I'm going to get started. This is a pretty dense talk, so I need every minute I can get.
00:00:34.880 If you go to my website, it's just my name dot com. It will have the slide deck, and if you want to follow along, that would be great.
00:00:41.440 Here we are, let's get started. First, I want to tell you a little bit about myself.
00:00:48.079 My name is Mike AbiEzzi, and I'm a software consultant. I work at Quick Left, which is a mobile and web application company. I live in Boulder, and I've been here for about three years. I love it here, and I'm very excited to be at Rocky Mountain Ruby presenting to you.
00:01:03.280 I'm also really nervous because I know a lot of you, so I hope I do well. I wish myself good luck.
00:01:10.080 Building a complex domain is a difficult thing to do. This is the book 'Domain-Driven Design,' written in 2003 by Eric Evans. It's a pretty dense book, and Eric is the guy who pioneered this concept.
00:01:24.159 Because he did pioneer the concept, this is the de facto book on the subject. If you haven't read it, I highly recommend it.
00:01:37.520 How many here know what Domain-Driven Design is? Good, about 30 percent. How many of you have tried to use Domain-Driven Design in a Rails application? Great! I'd say about 20 percent.
00:01:55.680 So, it's interesting. We're going to be building a Rails application using Domain-Driven Design principles. First, let’s get some basics out of the way.
00:02:08.560 What’s the domain? A domain can be a doctor's office with doctors, patients, and record keeping. It can also be a new idea, such as Twitter, which is a microblogging service with tweets, followers, and followings. The domain can be anything; it can even be collecting really cute cat GIFs.
00:02:26.239 The point is that a domain is the subject matter around which you're building the software. I’m glad to clarify that.
00:02:39.519 Why does Domain-Driven Design exist? Well, let's look at an example. Say we're building a Rails application with a few models and a small team of two developers.
00:02:52.480 We’re really productive, turning out features and feeling awesome! That's kind of what made Rails popular in the beginning.
00:03:05.519 However, over time, your Rails models get bigger—maybe 50 to 100—and your team also gets bigger, perhaps quadrupled. You begin to slow down.
00:03:17.360 There's more defects than there used to be, and turning out features becomes more difficult. A lot of us have felt this pain.
00:03:30.159 As a consultant, I see many client applications that have gone through this. They become big monolithic Rails apps that are hard to rationalize, making it difficult to get anything done.
00:03:40.720 So, we end up feeling like this turtle. The point of Domain-Driven Design is to address the fact that software becomes complex quickly and to create structure to tackle that.
00:03:55.680 What this talk is not about: There are many topics on DDD, and I want to set expectations with everyone.
00:04:01.360 It's not about custom architecture for Domain-Driven Design, design patterns specific to DDD, or advanced topics like what a bounded context is.
00:04:14.640 Custom architecture is expensive. If you're a startup, there's a reason startups like Rails—because it’s fast.
00:04:34.160 We don’t necessarily want to go custom right away. We may not have even proven our market—we might just throw away the app in the end.
00:04:46.400 So, we have to be very conscious about when we take on custom implementations. They’re all great, but there’s a time and place for it. What we are going to talk about is taking Domain-Driven Design principles and applying them to a Rails application.
00:05:07.680 We'll also talk about how to structure your Rails application in a way that allows you to transition to custom architecture in the future. We want to move fast, utilize Rails to its full potential, and be productive.
00:05:22.240 So, what’s this?
00:05:35.760 What does this tell us about our domain? Nothing. Perfect.
00:05:40.640 So, we’re going to try to fix this. We’ll discuss seven topics: first, how do we go about defining the domain and what that process is like.
00:05:55.680 We’re going to talk about how communicating the domain is important, especially with a large team, to survive that large team.
00:06:05.760 Next, we’ll discuss relationships, why they’re important, why they might not be important, and then we’ll talk about aggregates, data access, value objects, and domain services. We will explore what all of those things are and how to implement them in a Rails application.
00:06:30.800 So first, we need a domain. Let's pick a domain that we can build a Rails application around. I'm going to pick the iOS App Store, something we are all familiar with.
00:06:57.680 Let’s pretend that Apple executives, named Scott, came to me. He wants us to build this app store for him.
00:07:12.000 We’re going to sit in a conference room together with a whiteboard, brainstorming ideas for what this app store is about. Scott mentions, yes we’re obviously going to have apps; developers can create apps.
00:07:29.680 Any developer can create an app and submit several versions of it as they make iterations. They can also add one to six screenshots. Customers are going to be purchasing this app.
00:07:43.760 Customers can install specific versions and leave comments on a version of the app. So we did this brainstorm dump with Scott. We let him get everything out of his head. We don’t interrupt, just draw it all out and take a step back.
00:08:04.080 Now, we start looking at this and refining it, giving him a visual that we can work on. I start asking about developers. I ask, is it only developers that can create apps?
00:08:20.640 What about companies like game companies? Can they create apps as well? Scott confirms that yes, companies can create apps too.
00:08:34.560 So, I note these two terms down because the word developer wasn’t enough to capture that concept. I then ask him if there’s any difference between a developer and a company in terms of how they create apps.
00:08:47.120 He says no, they’re pretty much the same; they do the same thing. I suggest that a better word than developer or company is 'seller' because they’re both selling their apps.
00:09:02.080 Scott agrees that 'seller' works, which we’ll use going forward. Looking at version, he used the words version and release interchangeably.
00:09:15.040 I thought of versions; they have version numbers but it's awkward to say a version has a version number—it’s redundant. I hit on 'release' as being the right term.
00:09:28.080 I discussed it with him; he agreed that ‘release’ was better. When we look at comments, I ask Scott what else might users leave.
00:09:39.520 Are they going to leave ratings as well? He says yes, they’re going to leave ratings. So the term 'comments' isn’t sufficient. How about the word 'review'? It captures both comments and ratings.
00:09:56.640 Now we’ve refined our domain. We went through this exercise, considering the language we’re using and ensuring its accuracy for the domain we're trying to bridge.
00:10:09.440 Let’s talk about the finer points of communication we just demonstrated. There are two sides: you have your domain expert who knows how the domain works, and then you have the software expert who knows how to build software for the domain.
00:10:25.440 You go through iterative brainstorming and drawing diagrams while speaking out assumptions. It's very important to use natural language here, as you'll be doing a lot of filling in the gaps.
00:10:38.560 As the domain expert, you are looking for anything that feels awkward or is just not quite right for the domain. Meanwhile, the developer looks for terms that are being used to describe the same concept, ensuring you don’t use two words for the same thing.
00:10:55.360 We are defining what Martin Fowler describes as the ubiquitous language—which is a common, rigorous language between developers and users.
00:11:11.440 The need for it is rigorous, as software does not cope well with ambiguity.
00:11:24.080 From your user to your product owner, to your domain expert, tester, developer, designer, and back to your code, it’s crucial to ensure consistent language across the board.
00:11:40.320 Without this, you’re going to create a fragmented language, leading to confusion. More worrisome is that your code will start having fragments of language, and without understanding your domain, how can you build it?
00:11:57.440 If there’s nothing else to remember from this presentation, it's this: ensure you have one language throughout your team. If you change terminology in your UI, make sure everyone's on board and update your code.
00:12:10.080 It’s the only way to maintain control of a complex domain: communicate efficiently, and ensure that everyone understands it properly.
00:12:22.080 Next, we’ll talk about relationships. Let’s revisit our diagram and look at the relationship between an app and a customer.
00:12:30.640 We have the app that has many customers through purchases, and below we have customers who have many apps. We need to question which of these relationships are valid.
00:12:48.480 For instance, it makes sense that a customer should know about the apps they purchased in order to redownload them later—so that relationship is valid.
00:13:06.640 Now, do apps need to know all the customers who purchased them? You might come up with statistics reasons, but for our MVP, we want to keep it limited.
00:13:23.040 We don’t need a list of all the customers that created an app. Therefore, we’re going to nix that. Similarly, we’ll look at install relationships.
00:13:37.680 Does our app need to know which releases customers installed? All releases or apps will be installed on the customer’s device, so we can send a list of all apps with versions to the server.
00:13:54.240 The server can check which versions have updates, so we don’t need that relationship either.
00:14:06.640 Regarding customers and reviews, do we want them to view a list of apps they reviewed? As far as I know, iOS doesn’t have a mechanism for that, so we don’t need that either.
00:14:18.560 What we've done is distilled our domain down to just the relationships we need. The benefit is that we’ve decoupled things that didn’t need coupling.
00:14:29.440 Whatever relationships remain are the important ones that actually matter to the domain. When we’re looking at an application and a relationship, we know it has a specific purpose.
00:14:48.560 Now, let’s get into aggregates. Finally, we’ll get into some code. Imagine we’re submitting a new release of an app.
00:15:06.640 There’s a chunk of code here; we're creating a new release, passing in a version number, creating a release object, and adding screenshots to the release while setting its status to submitted.
00:15:20.760 Then, we add the release to our app. A better way to handle this is to encapsulate it in a method called 'submit_release.' That method name describes the domain behavior we are trying to represent.
00:15:36.320 Notice that the method resides on the app; the app controls the release and the screenshots, which makes sense because an app has releases.
00:15:57.120 It’s simpler. We have a straightforward representation of that action. Furthermore, this method centralizes the complexity of managing the submission.
00:16:13.680 Now we see what an aggregate looks like: in our example, the app is our aggregate responsible for managing things like the release, screenshots, and reviews.
00:16:35.680 Everything that is done with screenshots, releases, or reviews has to pass through the app. You never manipulate them directly.
00:16:47.440 By doing this, you're decoupling the small subsets of models from the rest of the system. If this is repeated, you will build multiple aggregates controlling their own complexities.
00:17:04.800 As we look at our app, we see all these domain behaviors: submit release, approve release, flag for abuse, mark as a staff favorite.
00:17:15.440 These methods help us understand what's important for the domain. They give insight into what can be done.
00:17:24.240 Let’s shift focus to data access in aggregates. I have an app, and I’m looking for apps with a where clause.
00:17:41.920 Here, I’m looking for apps that are less than a week old and have been purchased more than 10,000 times. While that’s clear, it can frustrate me.
00:17:57.440 I understand what you’re doing, but I want to know why you're doing it. I’ll give this a big fat 'wha' to express my confusion.
00:18:05.120 So, what’s a better way of doing this? Rails gives it to you! It’s called 'scopes.' A scope gives a name to the query that you have.
00:18:22.880 Now we know that the meaning of this query is 'new and noteworthy'—we want to know what the new and noteworthy apps are in the app store.
00:18:36.960 Another cool thing is ‘fig leaf’ created by Avd Grim. It can privatize all those Active Record methods like the where clause so they can’t be used outside the app.
00:18:50.560 This serves as a reminder that you should only be writing queries within scopes. Of course, developers can always find a way around that, but this is more of a reminder.
00:19:01.840 With great power comes great responsibility, right?
00:19:11.680 Now, looking at our app again, we have different ways to access data, including our behaviors—indicating what can be done with it.
00:19:27.680 We’re establishing one expressive point of entry for aggregates. Remember, aggregates are the domain's only point of entry for data access.
00:19:41.920 If you want information on releases, screenshots, or whatever that might be, you’ll always go through your app.
00:19:58.560 This will keep data access concentrated, which is pretty cool.
00:20:10.320 Lastly, let’s talk about value objects. This tends to be the most complex and lengthy topic, so I’ll roll through it.
00:20:28.240 There are two types of domain objects: entities and value objects. An entity is a thing; that’s the simplest way to describe it. A value object, however, describes a thing.
00:20:44.160 For instance, we may have a customer object as our entity and a name object as our value object. If I’m a customer, my name is Mike AbiEzzi.
00:20:59.680 If my cousin is also a customer and his name is Mike Abiezi, we’re still two unique entities. We have our own life cycles and can deactivate our accounts independently of each other.
00:21:09.760 Our attributes may overlap, but that does not mean we are the same. That's what makes an entity unique; it has independence, attributes, and a life cycle.
00:21:25.440 Conversely, a value object describes a thing. If I have a name value object and throw it away to create a new name object, it doesn’t matter.
00:21:39.440 This characteristic—being able to create and dispose without repercussions—is what defines value objects. This leads us to make them immutable.
00:21:56.560 Immutability is beneficial because entities can change state. You can create an entity or initialize it through a constructor, setting its initial state, which may change.
00:22:10.320 On the other hand, a value object has only the constructor, which establishes its state and then concludes.
00:22:24.080 They're not complex in terms of being built, making them easier to understand. Also, when dealing with value objects, nothing can accidentally alter their value.
00:22:39.920 In contrast, entities can change, leading to unforeseen side effects across different functions.
00:22:54.880 Value objects simplify design complexities. If we have 100 models and identify 30 as possible value objects, we can reduce domain complexity.
00:23:05.680 We don’t want to unnecessarily increase the complexity.
00:23:21.680 In our example, we know all aggregates are entities—these complex things that can build and change state.
00:23:36.560 A release has a life cycle; it gets created, and older releases become invalid as new ones come out. Now, what about screenshots?
00:23:52.080 Do they have a life cycle? They somewhat do through the release, but are they important independently? Not really; they merely describe a release, making them value objects.
00:24:06.880 Now, let’s look at reviews. If we look at reviews in the context of Amazon, you can edit and view lists.
00:24:20.560 However, for our app store, we want people just to create a review. If they want to update it, they create a new one, and we delete the old one behind the scenes.
00:24:36.480 So, we treat it as a value object. What does a value object look like? It's simply a plain Ruby object. It does not inherit from anything, especially Active Record.
00:24:52.160 It has a constructor for taking in attributes, exposes read-only methods, and overrides equality methods to ensure fast comparisons.
00:25:06.240 A good method for value objects is a factory method.
00:25:11.840 For instance, when it comes to a string, we can use the 'upcase' method to generate a capitalized version. It’s a very natural behavior for value objects.
00:25:23.920 Let me show you a value object that increments the major version to demonstrate a simple behavior.
00:25:38.400 The tricky part is understanding how to persist value objects.
00:25:45.440 We’ll discuss three ways: inline on the entity’s table, serialized on the entity's table, and in its own table.
00:26:04.000 First, inline on the entities table. Suppose we have a release table with attributes for major, minor, and build. We want to store value objects separately.
00:26:18.560 To achieve this, we create a writer method called 'version_number' that takes in a version number object and assigns its attributes to the release attributes.
00:26:34.080 When reading the version, you instantiate a new version number object with the release's attributes.
00:26:49.040 The second example involves screenshots stored as an array saved to a single column called screenshots on the release table.
00:27:05.600 You’d accomplish this with a JSON serializer and deserialize when needed. This is a very basic example; for arrays, a more advanced implementation would be required.
00:27:21.760 The third example is to store value objects in their own table. In our example, we have reviews. Unfortunately, these must inherit from Active Record.
00:27:38.080 There’s a method in Active Record called 'read-only bang'. It locks your model, preventing the alteration of attributes after being set.
00:27:54.240 However, it can still be awkward—allowing you to set attributes before raising an error. Thus, we’ll create a method that raises an error when attempting to write to an attribute.
00:28:09.760 We’ll wrap this into a method called 'immutable,' so it can't change after saving, ensuring that any changes recreate the value object.
00:28:20.160 Lastly, let’s discuss domain services. Aggregates control their own complexities. If we need to process transactions between two aggregates, we require a third party.
00:28:37.600 This is where domain services come in. For instance, if we want to gift an app, we need to charge the gifter and assign access rights to the giftee.
00:28:58.640 Both the gifter and giftee customer objects are aggregates that can’t directly interact; therefore, we utilize a domain service to facilitate this cross-aggregate transaction.
00:29:14.760 The domain service is a simple Ruby object. Its name describes the behavior the domain fulfills.
00:29:27.760 You define an execute method to handle the transaction, taking in the app, gifter, and giftee as parameters, creating purchase records and access rights.
00:29:44.080 You would often want to utilize database transactions, but this gives you an overview of structuring these services effectively.
00:29:59.760 Now looking at our Rails directory again, we create a services folder that includes all these domain services, enabling oversight of these cross-aggregate transactions.
00:30:10.320 In summary, we talked about how to continuously refine the domain with a domain expert. We emphasized the necessity of a ubiquitous language for seamless communication across your team.
00:30:25.760 We discussed constraining relationships to only the necessary ones, creating aggregates to manage complexity, and express domain behaviors.
00:30:40.080 We reviewed how to express domain intent through well-defined data access, create value objects to eliminate unnecessary complexity, and establish domain services.
00:30:54.160 Looking again at our Rails directory, we started with just Rails code. Now, we have defined our domain clearly.
00:31:07.840 Here, we have aggregates, all the entry points of our application, and know where to retrieve data and enact domain behaviors. We also understand all the services that define transaction behaviors across aggregates.
00:31:23.200 So we’ve effectively built a Rails application that reflects our domain.
00:31:32.160 Thanks for listening! I’d like to thank Quick Left for sponsoring me. We're hiring, so if anyone's interested in working at Quick Left, come talk to me.
00:31:46.560 A special thanks to Paul Rayner from VirtualGenius—he's been a kind of Domain-Driven Design mentor for me.
00:32:00.320 I’m available online and looking to find a few people to mentor across different topics. If you’re interested, feel free to visit my website and go to the mentor section.
00:32:18.080 Now, are there any questions?
00:32:25.760 So the question was if I could expand on how value objects keep us out of trouble. It's simple: they simplify objects to a point where it's hard to mess up.
00:32:51.440 If they can change, then managing state could become quite cumbersome, creating complexity. However, a value object keeps everything controlled in one method.
00:33:06.720 Additionally, when you pass a value object through functions, it cannot be altered accidentally—reducing side effects.
00:33:21.680 So, if you have an object that can change state, it leads to unforeseen alterations through multiple functions, which can create confusion.
00:33:34.000 Another question was about translating this for an API, where it feels more MVC-centric. I believe it works perfectly for APIs, as you’re getting to the core of how your domain operates.
00:33:46.160 You will want to create your domain to represent that effectively. In fact, I find it potentially even easier.
00:33:59.360 Okay, great! Thank you, everyone!
00:34:02.560 So, cheers!
Explore all talks recorded at Rocky Mountain Ruby 2014
+18