Talks

Typical DDDomains in Rails Apps

wroc_love.rb 2022

00:00:16.480 It's always fun for me to invite myself to the conference. So here we are. Let me introduce the next speaker, Andrzej Krzywda.
00:00:28.240 Today, I would like to talk about Domain Driven Design, or DDD, obviously. I will present from the perspective of the code that we all write.
00:00:34.800 Yes, we all work on different project types. Some of you may work on e-commerce, which is quite popular nowadays.
00:00:40.879 Others might focus on edutech, such as courses, teaching, learning, and similar activities. So, your application probably fits somewhere there or maybe it's not that far from those environments.
00:00:53.280 There are different types of projects, but we still solve very similar problems. If you talk to each other and look at the codebases we share, you'll notice that the concepts are very similar.
00:01:04.400 We often encounter the same challenges in our code. For example, who here has a column or a concept of a price in your project?
00:01:10.400 I'm pretty sure many of you are lying if you don't have a price. What exactly does your tool do if it lacks a price? So, you certainly have a domain of pricing in your project.
00:01:31.280 Similarly, if you have a login, you likely have authentication. If you have an address, you probably have shipping. If you have a concept of available products or are involved in e-commerce, you definitely have a concept of inventory.
00:01:44.720 And if you have a concept of payment, you have a domain of payments. If you're working with tax rates, you might then have a domain of taxes. If you have a concept of invoices or dates, you have a domain of calendar.
00:02:10.239 So, show of hands, who here has had at least one of those concepts? Okay, how about more than three? Alright, so we have very similar concepts in our Rails applications.
00:02:30.879 I made a list of 14 core domains that are common across projects. There are probably around 20 of them in total, and typically you have many of those, or at least half.
00:02:43.280 The important domains are highlighted here. The ones in pink represent your so-called core domain, where your business is unique.
00:02:50.120 Generally, your business is not unique in terms of how you handle pricing, shipments, and so on. But typically, we have other areas that are less usual and where we should concentrate our efforts. The less interesting tasks are the ones we've done all the time and are less engaging.
00:03:22.480 We all solve the same problems in code, but we do it differently. The diagrams we see are similar everywhere, but the code varies significantly.
00:03:39.840 Why is our code different? Because our codebase is Rails. We hope it works; we have models, views, and controllers. Recently, views have transitioned into JSON views, but hopefully, with Hotwire, we will return to traditional views.
00:03:53.360 Now, who has service objects in their code? Yes, that's quite a lot. I like service objects, though at times I have my reservations. I must defend their use because ten years ago I was one of the advocates for service objects.
00:04:11.760 The use of service objects has sparked interesting debates in projects. I'm curious if you've had those discussions. First, how do you refer to them? Do you call them services, commands, operations, actions, interactors, or scenarios? There are many names for the same concept.
00:04:31.520 Let's talk about naming conventions. Do you have a class called 'RegisterUserService' or is it simply 'RegisterUser'?
00:04:49.120 How many debates have occurred in your team about this? Probably quite a few. How should we group them? Do some prefer to group services into, for example, a UserService class, containing methods like 'register' and 'delete'?
00:05:05.360 Now, the question arises: what should service objects return? Should it be 'nothing', 'the last record changed', 'true or false'—given that we like Active Record—or should it indicate success or failure?
00:05:24.960 I won’t provide direct answers here, but what about communicating failures? Should they simply return false or use exceptions? Remember that exceptions can be relatively slow.
00:05:48.480 Can they invoke other service objects? What about testing? Do you mock everything out, or do you test multiple services at once? These questions remain open for discussion.
00:06:02.560 Take, for example, the concept of parameter-driven development. Service objects typically take a bunch of parameters, which tend to flow through the entire system, ultimately being saved somewhere.
00:06:19.280 My honest take is that service objects, while useful, are not always enough. A decade ago, when I was exploring service objects, I referred to them as a 'gateway drug' into Domain Driven Design.
00:06:36.560 Such questions are valuable because they reveal how elusive the answers can be. Moving forward, I've observed various attempts at modularization in Rails projects.
00:06:55.840 Who here has tried microservices? I'm sorry for you guys. And who has worked with engines?
00:07:06.080 Before I get started, I’d say that using engines can be tricky. I've seen many projects where you’ll find that someone started with Trailblazer and then later removed it.
00:07:26.080 The trouble with all these choices is that while they can be beneficial, they often go wrong. There's a critical assumption that many of us erroneously make, namely that having one level of modularization is enough for our entire application.
00:07:37.760 We might try to partition our application into engines, thinking that one engine can encapsulate everything. There's a pertinent question about whether models and their related services should exist together.
00:07:53.360 The underlying issue, regardless of the modularization method chosen, is that there exists more than one layer of modularization within an application. In fact, you typically have at least two layers. This is a gross simplification, but I think we should categorize many aspects into application and domain layers.
00:08:22.480 Let’s take a closer look. In the app layer and domain layer, the application layer requires its own separation and modularization. For example, on this layer, you would find the admin panel, or the primary application, and possibly a mobile API, as well as any integrations with platforms like Shopify.
00:08:41.760 GraphQL implementation also belongs to this layer. In the application layer, we mainly work with controllers and service objects, which are fundamental building blocks. Service objects belong to the application layer.
00:09:01.440 Then we have the domain layer, where we deal with essential concepts like pricing, inventory, and the more interesting domains I mentioned earlier. The read models introduced by Pavel during his talk yesterday are those persisted view models.
00:09:24.240 They provide an eventually consistent overview of data. For example, our admin panel might want to display all products regardless of their availability, while the public-facing application shows only available ones.
00:09:48.160 In this layer, we can utilize read models for various applications—this could include financial reports, ledgers, and more. This separation of concerns extends to how we handle integrations, such as those with Shopify, which might also require service objects.
00:10:06.720 When it comes to defining domains—which is the focal point of my presentation—domains are essentially functions. Thus, we can think of commands as inputs and the resulting events as outputs.
00:10:27.840 For instance, if we consider a pricing domain, we would have granular commands. These might include setting a price, applying a discount, and activating a time-limited promotion. The outputs would be similar to the commands.
00:10:38.560 The next aspect of our application relates to the application layer that contains these service objects. It's perfectly fine to locate them in app/services or generate directories if you're using engines.
00:10:59.520 Form objects can also find their place here. However, this might come as a surprise: the domain layer should not belong to a Rails application. This is your business logic and should exist outside the Rails directory.
00:11:14.560 To illustrate, on the left side, we see a directory structure that lists domains, entirely devoid of Rails dependencies. On the right side, we observe a typical Rails application structure.
00:11:35.840 In the Rails application context, you might find service classes and model services grouped separately according to their respective domains. Importantly, the domains contained within my e-commerce directory do not rely on Rails.
00:11:59.520 They operate with minimal dependencies, and the only necessary dependencies are regarding events and commands. For demonstration, consider we have commands like 'set price', which requires a product ID and a price.
00:12:13.520 We also have commands to set percentage discounts requiring an order ID and an amount. Such commands and events remain concise. Furthermore, we need to publish these events from the domain.
00:12:36.320 In many cases, you could utilize aggregates, but you don’t necessarily have to. You can also implement a straightforward mechanism for handling commands and publishing events, supposing the command can be executed.
00:12:52.640 If a scenario arises, say when a user demands to set a percentage discount only if no prior discount exists, the logic may necessitate removing existing discounts before applying a new one.
00:13:10.240 In such cases, if it fails, it raises an exception. Here is an illustrative piece of code that is relatively straightforward, as it shows a product class that permits price setting.
00:13:31.680 In this example, within the pricing module, we maintain an order that lists all products and applies discounts for that order. Crucially, it raises an exception if an attempt is made to assign multiple discounts.
00:13:54.560 This illustrates how we would implement typical domains, including your unique domains, through the use of input commands, output events, and the domains themselves.
00:14:13.920 Read models are typically simple, often confined to a single database table, as they focus on displaying specific boxes of data.
00:14:35.520 I've developed a DSL recently for read models where we declare the models. For example, a read model can subscribe to events like 'product registered', 'product name changed', or 'stock level altered'.
00:14:50.880 Event mappings occur across different namespaces and modules, which is why read models fit into the application layer rather than being tied to specific domains.
00:15:02.560 In this approach, we declare the mapping of event attributes to corresponding fields in the database, which offers simplicity.
00:15:21.600 Now, you may ask about how the domains communicate. Ideally, they should remain decoupled. While there are scenarios where domains need to collaborate, introducing unfavorable coupling creates complexity.
00:15:42.640 Domains should ideally operate distinctly. In business, there are processes that need to reflect concerns such as communication to ensure business logic accurately mirrors real-world operations.
00:16:06.560 Processes coordinate these domains, linking events to commands. At a high technical level, these processes are simply a means to react to business operations.
00:16:23.520 For instance, consider a simple checklist scenario where, if certain conditions A, B, and C occur, we execute action X. A practical example might involve notifying an invoicing department whenever a product's name changes.
00:16:43.680 When we name products, the invoicing department must be made aware so that the correct title appears on invoices. In some projects, there’s a separate name exclusively for invoicing.
00:17:06.720 As such, we have a product-renaming process embedded in our codebase that triggers updates within the invoicing system.
00:17:21.840 Another process might involve whenever users add items to a basket; this triggers a shipment preparation step, which is common in certain fast-food chains' ordering systems.
00:17:34.160 The unique aspect here is the synchronization of data across domains, allowing for smooth transitions without compromising business logic.
00:17:51.600 For our e-commerce simulation project, we've identified many such processes—over 25 to 30—that facilitate communication between domains, enabling better reusability.
00:18:07.680 Returning to service objects, we have a user interface with forms for product details like name, price, and tax rate. How many commands do you think can stem from these three controls?
00:18:20.480 The intuitive answer may suggest three, but in reality, we're looking at four. Often, the code we use showcases controllers, and service objects become less relevant.
00:18:35.440 Frequently, these service objects serve merely as mappers from parameters to commands. This leads me to consider whether we should just keep this logic in controllers.
00:18:51.600 Method calls may generate commands, projecting them into a structure. Service objects primarily exist to connect the commands we want to run to an appropriate domain.
00:19:06.560 The concept of service objects returns us to our original debates. What should a service object return? In Java, it would be 'void', but since Ruby lacks this concept, we typically ignore the return value.
00:19:23.440 For testing, you merely pass params and observe what commands were created on the command bus. It's worth noting that the distinctions between ‘call’ and ‘execute’ are negligible.
00:19:35.680 As a summary of conclusions, we see ongoing debates about how to prepare juniors for Rails applications. This is often argued against applying DDD in Rails, as many feel a big ball of mud is easier for juniors.
00:19:54.080 However, I would argue the opposite. Juniors may find it significantly easier to start with simple tasks focusing on one remodel at a time, gradually increasing in complexity.
00:20:11.920 While I'm not a big fan of estimates, modular codebases lend themselves to easier estimation because you can directly map requirements to components.
00:20:30.960 While achieving this level of modularity can be challenging, it’s worth striving to recognize typical domains within your codebases right from the start.
00:20:47.680 Even if you don't yet have reusable components, recognizing them can help pave the way for future improvements.
00:21:02.560 This perspective reflects a more forward-thinking approach, and I hold hope that five years from now, we will have open-source gems available for various domains.
00:21:20.720 It is essential to configure these domains with the processes we have discussed earlier.
00:21:41.680 Moreover, read models prove to be compatible with new offerings like Hotwire, allowing us to easily connect ActiveRecord with modern frameworks.
00:21:59.520 Each project I examine often reveals tempting dependencies between read models, which can lead down a tricky path. The more decoupled they remain, the better.
00:22:17.760 For the sake of creating a more beginner-friendly environment, separation can provide clear direction for understanding.
00:22:30.080 To reiterate, service objects should primarily be mappers translating parameters into commands. Commands act as inputs into domains while events serve as outputs.
00:22:47.440 Events trigger business processes, leading to new commands that subsequently engage read models.
00:23:09.440 I recognize that many concepts may come across as abstract or challenging initially. But this thorough event-driven approach can genuinely ease your workflow.
00:23:20.560 Form objects, in particular, should refrain from directly saving data. On this note, I wouldn’t want to miscommunicate what these controls are intended to do.
00:23:38.320 Parameter-driven development should not become the norm where we pile parameters into controls inappropriately.
00:23:56.960 Entities like Rails engines belong to the application layer. Active Admin and Avo are acceptable only when they allow connections to service objects or commands.
00:24:15.760 Thank you for engaging. If you're utilizing tools such as Active Admin or Avo, I have reservations regarding their functionality.
00:24:32.080 For instance, I once tried to use Active Admin but ended up deleting it as it attempted to introduce extensive abstraction that I found counterproductive.
00:24:54.000 While there’s great appeal to speedy development, later modifications can create problematic scenarios.
00:25:05.560 Identifying how the requirements correlate to building blocks is crucial, and through this clarity, you foster a better understanding of domains, including their roles in your applications.
00:25:21.680 Unit testing becomes much more attainable when inputs and outputs are explicitly defined. The focus here is essential for maintaining organized structures.
00:25:38.880 With clear interaction points between services, you empower a more streamlined testing approach, which ultimately benefits all involved.
00:25:57.920 In parting, let’s not overlook how we handle data concerning GDPR. When discussing encryption, the focus shifts to safeguarding the integrity of sensitive data.
00:26:14.960 Managing the keys related to encryption can get tricky, so we must carefully consider where those keys are kept.
00:26:30.960 In our projects, we may decide to omit certain keys from backups or store them in a secure environment.
00:26:48.320 While some events may contain sensitive data, it is crucial to manage them securely to maintain compliance.
00:27:06.480 I would like to hear your thoughts on the migration of legacy systems towards derivatives, as these transformations can become significant.
00:27:26.880 The risks of misidentification of domains often lead to rework, especially when modernizing microservices or other approaches.
00:27:44.320 From experience, you should remain cautious of modularization when lacking clear domain definitions. If domains are mischaracterized, pitfalls await.
00:28:01.840 The good news is that with knowledge of generic domains like pricing and payments, you can take proactive steps from the outset.
00:28:17.640 Let's not confuse broad development perspectives with critical specifics; it's vital to base choices on observable data.
00:28:34.720 Another thought I appreciate is how DDD can benefit junior developers, as the modular approach clarifies individual components and reduces structural clash.
00:28:50.880 I'm pleased to have taken you through this discussion, and my hope is to apply these experiences in developing further tool applications.
00:29:07.840 If you wish to explore, please engage carefully with these concepts; they provide an enriching opportunity to evolve our development practices.
00:29:24.000 I trust you appreciate how dense this content may appear initially, but with patience and practice, clarity will emerge.
00:29:40.960 Thank you very much for your attention. I look forward to any remaining questions and those who wish to challenge ideas can welcome further dialogue.
00:29:58.640 Now, let’s open the floor to your questions.
00:30:25.280 When discussing Rails, there is the assertion that certain concepts become constraints towards efficient architecture, leading to decipherable confusion.
00:30:41.680 DHH began promoting aspects surrounding modularization, leading to ongoing considerations about architectural restructuring.
00:30:57.040 Your point about AOP failure resonates; we often seek to enforce structure while remaining mindful of modularity.
00:31:12.000 At the core, the consideration between event-driven and traditional approaches introduces nuanced dynamics into overall data management.
00:31:26.800 Considering your input on how processes evolve, one can see the need for proper oversight when restructuring databases and event communications.
00:31:41.440 This continued dialogue might well facilitate fostering fresh perspectives on building with Ruby on Rails.
00:31:56.320 I'm grateful for your feedback and to continue this conversation about the dynamic interplay between DDD and frameworks.
00:32:11.680 Maintaining open communication is essential for advancement, so feel free to reach out to explore these insights further.
00:32:28.000 I appreciate everyone’s engagement. Thank you! I look forward to what lies ahead.