We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Building Beautiful Systems with Phoenix Contexts and Domain-Driven Design

00:00

Formal Metadata

Title
Building Beautiful Systems with Phoenix Contexts and Domain-Driven Design
Title of Series
Number of Parts
11
Author
License
CC Attribution - ShareAlike 3.0 Unported:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this
Identifiers
Publisher
Release Date
Language
Producer
Production Year2018

Content Metadata

Subject Area
Genre
Abstract
Phoenix contexts are a powerful code organization tool - but without a clear idea of what business domains live under the hood of your systems, naively creating contexts leads to over-engineered, fragile systems. Today, we’ll learn about the philosophical roots of Bounded Contexts from the hard-earned wisdom of Domain-Driven Design. We’ll quickly get our hands dirty in the nuts and bolts of a Context Mapping exercise, a strategic design tool that reveals domain-specific system boundaries. With our newfound architectural vision, we’ll learn how to write context-driven Phoenix code and develop some organizational rules around communication, boundary enforcement and testing between contexts. We’ll leverage the unique powers of Elixir that make this style of architecture so natural and see how using contexts easily leads to highly-cohesive and loosely-coupled outcomes! About Andrew: Andrew is a software engineer at Carbon Five, where he builds digital products for companies of all shapes and sizes. He enjoys trail running anywhere there are mountains. Though he currently lives in Oakland, he dearly misses his years living in (and eating through) Los Angeles.
System programmingBuildingDomain nameMereologyOrder (biology)Multiplication signLattice (order)Roundness (object)System programmingSlide ruleComputer animation
Domain nameSystem programmingAxiomKolmogorov complexityAbstractionScale (map)Modul <Datentyp>Encapsulation (object-oriented programming)BuildingContext awarenessFunction (mathematics)Local GroupCohesion (computer science)Execution unitImplementationIdentity managementEmailString (computer science)TimestampModule (mathematics)Game controllerOperations researchPrice indexVolumenvisualisierungInterior (topology)Interface (computing)LogicBoundary value problemSystem programmingProjective planeMultiplication signClient (computing)Cohesion (computer science)SoftwareIdentity managementModule (mathematics)Web 2.0Domain nameLogicCore dumpQuicksortInterior (topology)Group actionImplementationFunctional (mathematics)DatabaseSystem callEmailCuboidFilm editingInterface (computing)Flow separationVideo gameBitCase moddingContext awarenessProcess (computing)Texture mappingProgrammer (hardware)CodeEncapsulation (object-oriented programming)Disk read-and-write headSampling (statistics)Goodness of fitReal numberAbstractionComputer programmingComplex (psychology)AreaBuildingMathematicsBoundary value problemFamilyGame controllerHuman migrationComputer fileCartesian coordinate systemComputer animation
SoftwareDomain nameAbstractionCore dumpSet (mathematics)AbstractionNoise (electronics)Web 2.0Software design patternBoundary value problemCodeDomain nameLatent heatMultitier architecture.NET FrameworkCartesian coordinate systemGoogolWeightJava appletComputer animation
Personal digital assistantTerm (mathematics)Domain namePerformance appraisalInclusion mapMathematicsWebsiteCalculusSystem programmingCalculationOperations researchDatabase transactionPurchasingProduct (business)SoftwareFormal languageContext awarenessTexture mappingSelf-organizationEvent horizonLocal GroupGroup actionCluster samplingNatural numberCore dumpAreaFocus (optics)Moving averageProduct (business)Domain nameDirection (geometry)Mathematical optimizationContext awarenessTexture mappingCartesian coordinate systemProgram slicingAreaAnalytic setFormal languagePerspective (visual)CASE <Informatik>Type theorySystem programmingCore dumpCalculationGroup actionSimilarity (geometry)Event horizonVertex (graph theory)Gene clusterMultiplication signTerm (mathematics)SoftwareUniform resource locatorElectronic mailing listSoftware developerOperator (mathematics)Database transactionPoint (geometry)NumberBuildingBusiness modelDiagramCircleWebsiteSoftware testingExistenceComputer animation
Domain nameCore dumpData conversionGene clusterWeb browserAreaContext awarenessGroup actionIncidence algebraComputer animation
Term (mathematics)Domain nameFormal languageContext awarenessSystem programmingSoftwareComputer fontContext awarenessEncapsulation (object-oriented programming)Service (economics)Computer clusterElectronic mailing listBit rateCodeGradientMechanism designCore dumpSoftwareQuicksortWordWikiTerm (mathematics)MereologyModule (mathematics)Formal languageThermodynamisches SystemGoodness of fitSystem programmingMultiplication signMobile appComputer animation
Domain nameFormal languageModel theoryProduct (business)SynchronizationFormal languageModel theoryDomain nameDrop (liquid)Line (geometry)System programmingSingle-precision floating-point formatComputer animation
Domain nameModule (mathematics)Group actionNormed vector spaceSoftware repositoryMessage passingVolumenvisualisierungError messageContext awarenessIdentity managementAuto mechanicAuthorizationGame controllerInterior (topology)Formal languageDomain nameContext awarenessGame controllerCodeBit rateShared memoryGroup actionWeb 2.0Code refactoringBitFormal languageReal numberIdentity managementBellman equationDatabaseMechanism designExpressionForm (programming)MultiplicationWeb pageBoundary value problemImplementationComputer animation
Domain nameIdentity managementAuto mechanicContext awarenessComputer configurationDirected setNumbering schemeModel theoryModule (mathematics)Data conversionStrutBoundary value problemComputer wormGroup actionType theoryPattern matchingPattern languageBoolean algebraCollaborationismExecution unitPersonal digital assistantString (computer science)Software repositoryElectronic mailing listCommercial Orbital Transportation ServicesMaxima and minimaRootContent (media)Event horizonImplementationContext awarenessBit rateMereologyLatent heatSystem programmingPower (physics)View (database)Representation (politics)RoutingFormal languageNumberState of matterCode refactoringCodeType theoryConfidence interval2 (number)Link (knot theory)Event horizonComputer configurationPoint (geometry)Identity managementCASE <Informatik>Message passingDomain nameProcess (computing)Model theoryEmailDatabaseData conversionSound effectRight angleAnalytic setLanding pageInternet service providerGroup actionMotion captureBus (computing)LoginBoolean algebraPublic key certificateFunctional (mathematics)Pattern languageCollaborationismInverter (logic gate)Texture mappingMechanism designBuildingRootGraph (mathematics)BitNP-hardService (economics)String (computer science)Key (cryptography)Control flowNetwork topologyData structurePattern matchingAuthenticationDynamical systemSystem callComputer data loggingComputer animation
Google AnalyticsMetric systemContext awarenessIdentity managementEmailAuto mechanicDomain nameLibrary (computing)System programmingEvent horizonSource codeModule (mathematics)Process (computing)DivisorMultiplication signPattern languageSource codeDampingTexture mappingEvent horizonGroup actionContext awarenessEnterprise architectureFrequencyLevel (video gaming)File formatBus (computing)Complex (psychology)Library (computing)Identity managementSystem programmingMoving averageElectronic mailing listEmailCorrespondence (mathematics)Message passingComputer animation
Context awarenessCore dumpFormal languageCodeDomain nameSystem programmingScale (map)Hill differential equationBoundary value problemKolmogorov complexityScalabilityEvent horizonData integrityType theoryPattern matchingDomain nameScaling (geometry)Type theoryInternetworkingSystem programmingProgrammer (hardware)Event horizonFormal languageSoftware developerBitBus (computing)Pattern matchingMultiplication signPower (physics)Pattern languageBoundary value problemGoodness of fitComputer animation
Coma BerenicesJSONXML
Transcript: English(auto-generated)
I'm really happy to be here. My name's Andrew, and I used to live here in Los Angeles. My wife and I lived here for a couple years, and we were on the west side, and every time we wanted to get out here for Korean food in Koreatown or
something like that, we would get on the freeway and sit about 45 minutes in traffic, and get to dinner by like 9 p.m., but it was worth it, and there's really nothing like that up in the Bay Area, so we really miss our time here in Los Angeles. Now, I actually used to work in Santa Monica, and I'm really happy to say that I was able to be
a part of the LA Elixir meetup as it just got off the ground, and back then we were just meeting in a little conference room, and now it's really cool to see the LA Elixir community all get together here, so give yourselves a round of applause, because it's really awesome to see this at the largest LA Elixir meetup in history.
Alright, so, pardon me while I get my slides in order. Alright, so, at Carbon 5, we build a lot of systems, and we work with a lot of clients, big and small, and we basically jump into projects where we see systems that are old, or legacy systems, or systems that just have a lot of cruft.
And so, every time we kind of come into a system, we oftentimes see that there are a lot of difficulties running and managing these systems. Raise your hands if you've worked in a large monolith, or a legacy system, or just a system of any sort of complexity. And now keep your hands raised if you really want to tear your hair out every single time you're
in this system. That's right, because all of us know that maintaining really large systems is just really hard. But also what I want to get at is that it's really difficult to work in these systems. We should kind of cut ourselves some slack, because no one really is sitting there in a corner saying, wait till Wally sees what turd I'm going to put in this system.
You know, we're not sitting there intentionally making life difficult for others, and if you're on a team where you actually are doing that, I recommend some group therapy. But the bulk of complexity is really accidental. It really happens when we're shipping
things fast. We need to keep the business afloat. The customer needs to be satisfied. And we can't be faulty necessarily for delivering features to the customer. But you do this over and over and over and over again, and eventually little abstractions that used to work for small systems kind of topple over when
you start to get to a system that's maybe 10x or 20x larger than what it was meant to be. A lot of times, or essentially what our jobs as programmers are, are to work and build and improve upon abstractions in the system. Working with abstractions allow us to encapsulate complexity, push it off to the side, and help us keep things in our head.
Today, what I wanted to talk about are Phoenix contexts, which are one tool that the Phoenix core team has given us to deal with complexity in our systems. But beyond that, I wanted to introduce the concepts of domain-driven design through an exercise called the context mapping exercise.
And finally, we're going to close it up by walking through some real code samples in Elixir to kind of see how that might look in our real systems. So let's first go through the Phoenix context. What are contexts? Contexts are simply Elixir modules that group system functionality together by business domain.
In fact, this isn't necessarily even a Phoenix thing or a context thing. This is just a good programming abstraction thing. And the goal with the context are twofold. One is we want to keep cohesion within our systems.
So by grouping like concepts together, these concepts can change together, and they're all localized in the same areas of the system. You don't have to make a change in one file and run across the other side of the system or open another application to change a related concept. They're all in there together, so the idea is that you keep them close together because they're probably going to change.
Second of all, these modules are meant to hide internal implementations from the outside world and vice versa. When you're an external caller and you're calling into a context, you don't need to know how the sausage is made on the inside. Really, you don't care if it's persisting to a database or making a network call.
The idea is that a lot of this stuff is hidden away from you. Let's really quickly run through what generating a Phoenix context looks like through the scaffolding. And by doing so, maybe we can get an idea for what the Phoenix core team had in mind for us.
So over here, I'm running this command, and what it's going to do is it's going to create a user resource within an identity context. More on that in a bit. Additionally, what I'm saying is my user resource has these attributes, a name and an email. So I run the command, and out pops one Enecto schema.
You'll notice that it's a user resource that's nested inside of this identity module. And then I'm going to get this identity module, and in this module definition, I'm going to get a bunch of CRUD actions that allow me to modify this resource.
Within here, you're going to see calls to Ecto, and you're going to see a lot of the specific underlying implementation details to do so. Next, I'm going to get a web controller, and in this controller, it's going to live outside of my identity context. It's going to live in the web context.
And essentially what that web context does is it intercepts an HTTP request, and then it turns around and it calls across to the other context. It calls to my new identity context, and it says, hey, do your thing, but give me what I'm asking for. And finally, we get a migration. And so, just by looking at the files that come out of this migration, or out of this scaffolding command,
we can already see the general philosophy of this approach. All web concerns will live in the web context, and context should encapsulate their persistence in domain logic. So, right out of the box, the core team wants us to keep those two things separate.
And finally, the outer context module is the public interface into that module for the rest of the system. Well, this kind of makes sense. I guess it makes sense, but I do have a few more questions. For example, what should I name the context? How did I come up with the name identity?
Like, couldn't it have been something else? How should I think about resources that are needed in multiple contexts? How do I know if it's too broad, or if it's too fine? And is it easy enough to change? And how do I know if my context, when will I know when it's getting too big, and it needs to be split up, or does it even need to be split up?
And so, we come to this core question of how do we design system boundaries? This is a time-old question that we've debated over the years, and it was introduced, the book Domain Driven Design was published in 2003, and it really helped us think about designing system boundaries.
Now, when you go Google Domain Driven Design, you're going to get both a set of high-level strategic design activities and concepts, and you're going to get these really low-level specific concrete software patterns all in .NET or Java or what have you.
And it's going to be really confusing, and there's going to be a lot of noise, and I don't want us to get tripped up in a lot of those things, because Domain Driven Design has a lot of ideas. Many of them are amazing, some of them are not necessary to the core ideas.
And today, what I want to get us to understand and appreciate are the core ideas of Domain Driven Design. Typically, when we think of abstractions, we think in horizontal layers of abstractions. We want to make a tiered application, we're going to have a web layer, we're going to have a domain layer, we're going to have a persistence layer. That's typically how we think about modularizing our code.
But what if we took a bigger picture, and what if instead what we did was we thought about vertically decomposed business use cases? What if we broke up our application in vertical slices?
And many of you who practice maybe an agile design methodology or you're into behavior-driven development, you're already used to thinking about building features in vertical slices. So what if our systems more consistently applied those vertical slices? Today, you're going to all join me in my imaginary company.
It's called AutoMax, and it's a used car marketplace. And the way it works is that sellers come to our locations, and they bring in their cars for inspections, and we basically take them into their garage and give their cars a rating and appraise their value, and then we make them an offer. We buy their car.
Their car is sitting in the lot, we post it online, an interstate buyer comes in and says, hey, I want to test drive that Ford Explorer, I want to test drive that Tesla or something. And they come in and they test drive the car, if they like it, they'll buy it. And so that's our business model, we're the middle man, and we operate like this two-sided marketplace. Now, our business is constantly changing, and so our tech stack is constantly changing as well.
For example, just in the past week, marketing wanted us to change copy on the website. The finance team wanted us to change how we do tax calculations. The operations team needs a new feature in the vehicle inventory system.
The product team wants to do stuff in Bitcoin, I don't know why. And then customer support wants us to build a better support dashboard. Well, yeah, I mean, it's going to happen, your business is going to put demands on your tech team and your software systems, and we need to be able to react to them very nimbly.
And so the core ideas of domain-driven design are to listen to the business. I'm going to save everyone a lot of money, you don't have to buy the book, because I'm going to summarize it in these two points. Number one, design your software systems according to business domains. Sounds obvious. And then two, pay attention to the language you speak in the business.
When I first read that, I was like, what? What does that mean? Like, the language? And it turns out that domain-driven design cares a lot about linguistics. Just kind of like where M is coming from, but from a different perspective of the business. Literally, we need to listen to the folks in the business who are talking about the things they want to build.
And this brings us to our exercise of context mapping. Context mapping is a real-life exercise where you have to actually talk to people on your team, and you have to actually get people in a room, and you have to listen and discover all the concepts at play in your systems.
So, it's really important, let's say you and I and our team and our product owner and our on-site customer and our business stakeholders and what have you, we get together in a room one afternoon, and there we put up a big roll of butcher paper on the wall,
and everyone gets a bunch of sticky notes. And on these post-its, we give everyone the directive to write down all the nouns and the verbs that are present in this area of business. So, let's imagine everyone's having a good time scribbling stuff down,
slapping those post-its on the wall, and they kind of get this type of system. This is a rough idea of what a potential activity could lead us to. But you'll notice that we have both entities, the nouns, and we also have the verbs of what happens to those entities.
Most likely, it'll be far larger, more complicated, way messier. You'll probably have duplicates, all that stuff, but that's okay. The idea is to get everything up on the wall. Then what we're going to do is we're going to group similar concepts together. You're going to notice some things just naturally seem like they belong together.
For example, a website visitor in an online listing are probably things that are similar in their business purposes. So, I've taken out some of the events because they were a little too cluttered, and I've suggested that maybe this is what happens
at the end of our grouping exercise. Now, we take a step back, and we start to observe that perhaps there's some natural clusters or some groupings that might be starting to emerge. Stepping aside now, let's do some definition of some terms.
In domain-driven design, there's a concept of something called the core domain. This is what your business is here for. This is the main purpose that our company exists. For AutoMax, our core domain is car sales. What we're going to do is we're going to actually delineate that domain
on our diagram by drawing a large circle over it. Second of all, we're going to have a supporting domain. A supporting domain is an area of the company that isn't directly correlated with our core domain, but it helps it accomplish that goal indirectly.
For example, you could have things like online listings, or you might have financial transactions or maybe optimization and analytics and customer support and all these good things that really help the business do what it needs to do. I'm going to suggest, I will put forth that actually these natural clusters
that we're starting to observe are actually the subdomains if you don't have an idea of what they are. Magically, it just so happens that we've identified, perhaps together as a group, what some of these subdomains might be.
Of course, it will also never be this clean. Very likely you'll find duplicate concepts between other areas, and you'll have these discussions of things like, wait, is this the right name for the thing in this context? For example, in customer support, you might have this conversation
where you might be like, do we call it a support incident, or do we call it a support ticket? Are they a customer or are they a browser or something like that? Anyways, you're going to have a lot of these discussions, and it's very important to capture these discussions and put them up on the wall. Because at the end, what we're going to come up with is something called a ubiquitous language.
In domain-driven design, the idea is that there's a language that the business speaks that must be captured in our code. In an ideal world, the words that roll off your business partner or business stakeholder's mouth or the stories that are written in your backlog should correspond and cleanly map to concepts that are captured in your code.
A lot of times, we put these sort of terms and terminologies into a glossary, which is a fancy word for basically a list of terms and definitions. You can throw this in a wiki or a Google Doc, and then the idea is that you keep this updated
and you have the team continually refer back to it. And finally, we get to the idea of a bounded context. This term is kind of what the core team had in mind for the term context, and it's got a few nuances to it. So, concretely, a bounded context is a encapsulated software system,
so it doesn't necessarily need to be an app, although it could. It doesn't necessarily need to be a service or an API, although it could. It could also be something like a module or a package or a Ruby gem or just anything that is something that can live on its own independently
and run as real software. But, linguistically, there's an even more important definition of a context, and it's a boundary for where a term, a concept, or definition is allowed to live, but it only lives in that context.
And the reason why is a little subtle, but I'm going to go into it just right now. For example, let's say I have a concept of a rating. A rating is something that our mechanics give to our vehicles in our inspection context when the vehicles are in the garage. They rate the car, and they give it a grade of okay, good, great, fair, mint,
or something like that. But, however, in the other part of the system, in the customer support subdomain, a rating is something the customer gives us. Hey, rate your experience with us. Was it good? Zero to ten. Please tell us how your experience was. And these two concepts have the same name. They're overlapping, but they actually belong.
They deserve to be modeled on their own. Now, if we slapped everything together in one large software system, you might find yourself doing funny things like appending a prefix to the name of this concept and then doing something to the other concept to make them independent identities.
But, by the very fact that you're encapsulating them in their own modules allows you to express the fullness of what they are and to keep it really clear. So, this allows us to have precise language, precise models,
and our domains can express themselves as clearly as they need to be. So, what I want us to do is to really become language addicts. We're going to be really passionate about keeping our systems in line with what our business stakeholders are saying. And if there's anything that you take away from my talk today,
it may not be a single drop of elixir, but what I really want us to understand is that our systems should match what is being spoken in the business. Okay, so everyone be one with your domains. Okay, finally, we're at an Elixir conference, so we're going to actually go and talk about Elixir.
So, let's go start by looking at a Phoenix context. A Phoenix context, as you can see, exposes concepts to the outside world. So, over here, our inspection context is exposing a vehicle and a mechanic, but also, you can have your context express real actions that need to happen to do something in the real world. So, for example, we're going to add a vehicle to the garage queue,
so our mechanics can work off of the queue, or we're going to build a feature to attach a rating to a vehicle that a mechanic applies to a vehicle. So, we're going to start right here with the UI. This is something that, like a very simple web page, that a mechanic might pull up to go assign a rating to a vehicle.
And maybe in the old world, this is what the controller that backs that form action might look like. So, as you can tell, it's a little messy. It's got a lot of database concerns mixed up into the controller action, and then it's doing a few things here
that really are separate things that need to be broken out. I won't go too much into it, because we're about to go refactor this stuff. So, one of the things we're going to do is we're going to pull out the correct concepts into their correct boundaries,
into their correct context. So, for example, we're going to pull out the user first. The user moves into its own schema that's in the identity context, and then the identity context gets the ability for the outside world to go fetch it. Likewise, we're going to also do the same thing for the vehicle. A vehicle and a vehicle rating are both going to move into the inspection context
and be linked up together. And, finally, we're going to build an action in the inspection context to actually encapsulate the persistence of a vehicle rating, but also perhaps it's going to do some other internal things, like check a policy function to determine whether the user is authorized
to do this thing, to perform this action. And, finally, we then come back to our controller and we read it again. And so the idea is that a lot of these domain ideas and domain concepts are a little bit more fluent here in the controller action.
So, as you can see, we're now calling only out to contexts, and we're not really cluttering the web code with concerns that it doesn't need to know about. In my opinion, this is way better. So what do we notice here? Context only expose methods at their outer layers,
they hide internal implementations, and the domain language we use to express these concepts is encapsulated and captured in the code itself. All right, let's move on to a couple of other more advanced topics, the first of which is concept sharing.
So, we get in trouble when we need to use a concept in multiple domains. So, for example, very likely your systems have a user concept or an idea that is probably actually implicitly attached to multiple domains.
So, over here, we've modeled a user that's in the identity context. The user's job is really to capture the idea of logging in, of basically authentication. Perhaps it deals with OAuth credentials and yada, yada, yada. But, the idea of a user kind of breaks down when we move into the inspection context.
When that mechanic logs in to their part of the system, we actually call them a mechanic. And this mechanic actually, to that part of the system, it doesn't need to know about its OAuth credentials. There's actually a view of that user that really is mechanic-focused.
The same thing with marketing. Let's say that we're building a landing page, and really, a logged-in user is really just another website visitor. So, how do we capture the nuances of what each domain says about its own concepts, but kind of link them to the same thing under the hood?
Well, here's a few ideas for what we can do. Number one, we can just do nothing. We can actually just do things as we used to do them. So, maybe what we do is, in the process of refactoring, maybe it's okay just to have like foreign key references to a concept that's elsewhere in another context.
I personally think it's okay if you're refactoring, but I think there are a few other things you can do that might be a little better. The second thing we can do is something that I call strut conversion. So, the idea is that we convert external concepts into internal concepts at the outer layer. So, essentially, our outer context is going to have a function
that converts an external concept. In this case, we're going to take a user that's in an external identity context, and we're going to say, hey, I know about this external world. I'm going to go convert it to my internal representation
that is relevant to me and myself only, and then callers then will use my internal representation to then perform domain actions on myself. So, here's how then the caller might do it. It might start out with the logged in user. Once the system logs you in, you get a new user.
Well, now I need to call through the marketing domain, and the marketing domain needs a visitor, so I'm going to go first convert it to my visitor, and then I'm going to pass it through the other actions in my marketing domain. And here we can actually leverage
a lot of the powers of the Elixir ecosystem. We can really lean on pattern matching and types, and so this is one of the examples of using the full powers of Elixir, whereas in other dynamic languages, you may not have been able to do this. Even better, you can use type specs
to further enforce the correctness of the types of data that you're passing around, and to make sure that you're really only ever working with internal concepts that are valid to your part of the system. Finally, the third option is we can make what I call a collaborator schema. And so the idea is that perhaps you have a use case
where you need to both read and write to your internal domain model. Just doing a straight up struct conversion is not enough. For example, let's say my mechanic logs in, but I actually need a corresponding mechanic concept persisted to the database, because there's extra stuff about mechanics
that my user model cannot capture. So for example, maybe my mechanics need to be tracked whether they're contractors or not, or skills and certifications need to be also encoded or persisted. So over here you see I've created a schema for a mechanic, and what I'll do is now in my conversion function,
it's going to go and create, it's going to go create a mechanic as it converts from a user, and then it's going to go, we're going to add a conversion function that then looks it up from the database and then returns the corresponding idea. And these last two examples of what I gave you are what in domain-driven design terminology
would call the anti-corruption layer. We welcome outside concepts, but we convert them internally to be our internal concepts. Here's a little bit, here's a smaller side, and it's that I would encourage you to avoid cross-context joins if you can.
So for example over here, we have an internal concept, the visitor, that has a join under the hood in Ecto to the user. Now this is maybe not a big deal, but if you ever wanted to then do further refactoring or move away from Ecto, or even extract that context into its own API
or microservice, you would be in trouble. So perhaps one way you could do it is to avoid these hard database linkages and maybe just store the external foreign keys, but store them in a way that doesn't couple your two contexts together. So for example, you could now maybe expose it as a UUID
or even just store it as a string, and that way further callers or other engineers on your team will know that they're not really supposed to be linking these two concepts. A couple other ideas, the first one is called an aggregate. An aggregate is kind of a tree-like structure of data
that belongs together. Data that naturally belongs together should ship around together. Here's what I mean. Let's say in this inspection part of the system, we have a couple of internal concepts, and maybe if we ran the generator, we would end up with a context that had CRUD actions
that allowed us to update ratings and vehicles and list prices and all that good stuff. But do we really need all of those functions at the outer layer? Because now the outside world has extra stuff to worry about when it then turns around and wants to do things to our context.
So what if we just said, hey, outside world, you're only ever allowed to access the root of this graph. And I say that these concepts really only make sense when they're rooted on a vehicle. The vehicle is our aggregate root.
And so therefore, we can kind of simplify our APIs. So instead of the outside world needing to know about everybody, the outside world actually just gets the vehicle and then it traverses the root to go find the data that it needs. And so we'll end up shipping this graph around through our APIs or through a message bus
or what have you. Much in the same way, we also don't want it to write to specific parts of this graph, this aggregate. So over here, maybe somebody was trying to update the rating and then we allowed the outside world to know about the rating ID. Well, this is an internal implementation.
Instead, what we want to do is we want to allow the outside world to tell us, hey, I have a vehicle and I want to update the rating, but you know about the actual specifics about how to update it, so I'm going to let you deal with it. And so by leveraging aggregate roots,
we have the ability to minimize and simplify the APIs that we expose to the outside world. And they also have additional powers that allow you to see data for its full context. We're able to see, like, if you call an API, you can see the state of the system as an aggregate
and that will actually help you design your systems with more confidence about the synchronized states of all this data. Finally, this is maybe a more advanced topic if your system gets to this point. You can introduce event-driven messaging between your contexts.
So let's say I have part of my context here and it's going to go and do a few side effects after it completes. Let's say that when a user registers for an account, we then correspondingly create the right concepts in other contexts, and then we subscribe the user to an email,
and then we send some analytics event to some outside provider. This is fine and good, but then it clutters the code, and then now this code needs to know about the APIs of all these other contexts. What if instead what we did is we published events that is facts about the world over a bus?
Those of you who are familiar with the publish, pubsub, publish-subscribe patterns know that this is a nice way to kind of invert dependencies. So instead of the upstream caller needing to know about downstream side effects, we have the downstream consumers subscribe to events upstream.
And as a benefit, our context map actually already encoded the names of the events that we need to publish. So let's see what it might look like. Let's say that we publish an event. I like to follow a three-part format where I begin with the name of the context,
then I put a period, and then I say the name of the resource, that's the user, and then finally the name of the action in past tense. That's just my personal preference. And downstream subscribers will now subscribe to this event. Every time they see that event published over that topic, it's going to then expect a user struct
or a user map on the payload, and then it can do its thing correspondingly. So I'm going to use a library called EventBus. There are plenty other ways to implement messaging because Erlang is one of the best places to be implementing messaging. There are ways to do this with GenStage, there are ways to do this with RabbitMQ.
You choose your thing, but the overall sketches of the idea are here. So here, instead of calling downstream, my event source is going to just publish a fact over the bus. It's going to say, hey, I'm done, I created my thing, and downstream, whoever is downstream from me
can now do their own things. And now downstream context can now subscribe to it. So over here, my marketing event handler is now going to subscribe upstream to the user, and it's going to say every time a user is created, it's going to go subscribe to the user to the email list. And it's the same thing with our inspection context.
It's going to now create a mechanic from the user. And so this is a nice way to also further decouple what every context needs to know about other contexts. Of course, every architecture talk needs a bunch of caveats, and the first one is that we really need to start small.
If you have an existing system, or if you're starting from scratch even, don't try to do everything at once. Don't try to take all the patterns I just talked about and just build them, bake them in from day one. And the reason why is because you're going to find yourself kind of falling over from all this complexity that you threw in when your system didn't need it.
Also, if you have an existing system, you can take one context at a time and try it out. So you can start by extracting your identity context or your account context or something like that, and then you can build out small things from there. See how it feels? If it's a lot of pain or if it's overkill, roll it back. You don't have to do it.
Second of all, I want to caution us from also just kind of cargo-coulting a lot of the patterns we see out there on the Internet. Instead, pay attention to what the business says about itself. Use those concepts. Name things the way that they're spoken about. If we did all those things, I feel like our lives as programmers
and working in our systems would be way better. Sometimes the simplest things to do are also the hardest, so maybe we just have to get our hands dirty and start renaming. And finally, systems of a certain scale. I talked about this already, but sometimes your system's just not ready for it. Maybe you just got to wait a little bit
until you actually feel the pain before you start to implement the solution. So, in conclusion, we've listened to the business. We've made all the concepts say the same thing. We've defined strict boundaries between concepts. We've chosen to use specific language,
and we've made our domains more clear. We're using aggregates now to simplify APIs, and we're using an event bus to decouple publishers from subscribers. And finally, we're using the full powers of Elixir. We're leaning on types and pattern matching and type specs and all that good stuff.
Elixir's an amazing language to be working with, and it's a great time to be a developer in this community. And that's what I think makes a beautiful system. Thank you, everyone. Thanks a lot, Andrew.