Domain-Driven Design Book Club Notes

Domain-Driven Design Book Club Notes

Preface

Quotes

"As the team gained new insight into the domain, the model deepened. The quality of communication improved not only among developers but also between developers and domain experts, and the design—far from imposing an ever-heavier maintenance burden—became easier to modify and extend."

"The Extreme Programming process assumes that you can improve a design by refactoring, and that you will do this often and rapidly."

Part 1

Quotes

"It is not just the knowledge in a domain expert’s head; it is a rigorously organized and selective abstraction of that knowledge."

Model Usage

  1. The model and the heart of the design shape each other.

  2. The model is the backbone of a language used by all team members.

  3. The model is distilled knowledge.

Developer motivation

  • "Domain work is messy and demands a lot of complicated new knowledge that doesn’t seem to add to a computer scientist’s capabilities."

  • "Instead, the technical talent goes to work on elaborate frameworks, trying to solve domain problems with technology. Learning about and modeling the domain is left to others. Complexity in the heart of software has to be tackled head-on."

  • "There are systematic ways of thinking that developers can employ to search for insight and produce effective models. There are design techniques that can bring order to a sprawling software application. Cultivation of these skills makes a developer much more valuable, even in an initially unfamiliar domain."

Chapter 1 Crunching Knowledge and Chapter 2 Communication and the Use of Language

Quotes

"Design and Process are inextricable."

“The domain model will typically derive from the domain experts’ own jargon but will have been “cleaned up,” to have sharper, narrower definitions.”

“A document shouldn’t try to do what the code already does well. The code already supplies the detail. It is an exact specification of program behavior. Other documents need to illuminate meaning, to give insight into large-scale structures, and to focus attention on core elements. Documents can clarify design intent when the programming language does not support a straightforward implementation of a concept. Written documents should complement the code and the talking.”

“It takes fastidiousness to write code that doesn’t just do the right thing but also says the right thing.”

Discussion

  • Spoke about examples of various previous experience with DDD.

    • Working with domain experts and using software to solve their problems.

    • Working to define the vocabulary, interactions and contexts for the domain in question.

  • Question: The book presumes there is a domain expert.  What do we do when there is an ideal but not a person that embodies the domain knowledge?

    • Online education is different but it's still teaching.

      • The existing domain experts for teaching is a good starting place for domain expertise

      • Fallacy that any one domain experts knows everything you need to know.

        • All knowledge is based on constraints of the persons experience

      • Current experts can help but there will always be an iterative process on top of that.

    • Domain experts don't know what the software is going to look like when it's "Done", its the people trying to use your software today.

    • To find a domain expert we need to figure out what the job needs to be done first.  Then we can figure out what the domain is and what experts we need.

    • Don't rely on just one domain expert.  No one person will have the full picture.

    Question: Which edX features would have benefited with DDD? (Course modes, cohorts/teams/groups)

    • Course Discussions

      • Discussion IDs, Commentable IDs, etc

        • Different names for the same thing

    • What's the right level to pick a domain?

      • Too close and we make decisions for how instructors teach making it harder to experiment.

      • Discussion vs discussion experimentation platform

      • Are we a course content delivery system or course content delivery experimentation platform?

        • Per recent product discovery, general consensus is that we are a delivery system first and an experiment platform second.

          • Building for generic experimentation is very broad and hard to provide value with

  • DDD could be used to expose gaps where one domain has dependencies on other systems that may not be clear until we think of the problem from a domain view.

  • Question: Could we say that a domain expert for us is our learner base?

    • Using our learners and rapid feedback loops to iterate.

  • Relying on an agreed upon vocabulary is really important. (Not be-all-end-all but crucial none the less).

    • Applying DDD to new things is easier,  how do we do it for existing software.

      • Supposedly a chapter that addresses that we haven't gotten to yet.

  • Question: Looking at the messy areas of our code, would DDD have helped or was the problem space just not clear?

    • We didn't have answers to a lot of basic business questions.

    • How do we go from no answers to having a defined domain?

      • Start with the existing data and define all the existing entities.

      • How do we map them and their interactions together?

      • What assumptions are we making.

      • What data do we have to start with?

  • For us whatever the current implementation is, is the model but not documented.

    • Do we need to document the model of the current implementation or just define what the model should be?

      • We probably need both the model of what we have now and the model we want to move to.

        • Need both so that we can know what we are moving from and what we are moving to.

  • Question: Is there a DDD Skeptic?

    • Many people.

    • Still unsure that DDD would have prevented things that we have done in the past.

    • DDD is supposed to help give us a common language so that we improve time to value.

    • We pay for it later if we don't have alignment.

    • Can we actually put this into practice?

      • The more you practice, the better you get at it.

  • Question: How do we decide what's in and what's out?

    • Conversation with stakeholders and others, realizing that it could change.

  • We were already doing DDD to some extent - even as creating models mentally.

  • Not bringing all legacy code up-to-date with the latest model, helps with moving fast.

    • Which is fine, because the model doesn't need to be knee deep into the entire system.

    • If we're never really touching old code that was built with an older model, then it's Ok to choose not to update it.

      • Analytics team was able to do this because their "workflows" were independent bounded contexts.

  • Question: Are batched workflows in the Analytics pipeline considered a domain model?

    • Since "pipeline" is part of the deliverable, it's reasonable to consider "workflows" to be part of the domain.

    • Consider an "event" from it's baby stage through it's life cycle.

    • For example, when they created the "django framework", a "request" would be part of "django's" domain.

    • There can be multiple "domains" within a system:

      • User domains driven by business needs versus Technical domains driven by technical framework needs.

    • Since the Analytics pipeline has technical customers, the Technical domain models are first-class models.

  • Different edX engineering teams will have different models.  For example:

    • Educator versus Learner on what a "Course" and "Course Run" are.

    • Release pipeline versus Analytics pipeline have a different concept on what "workflows" are.

  • Later in the book, Bounded Contexts should make things a lot clearer, hopefully.

Chapter 3 Binding Model and Implementation and Chapter 4 Isolating the Domain

Quotes

“Software development is all design. All teams have specialized roles for members, but over separation of responsibility for analysis, modeling, design, and programming interferes with MODEL-DRIVEN DESIGN.”

Example: “A user of Internet Explorer thinks of 'Favorites' as a list of names of Web sites that persist from session to session. But the implementation treats a Favorite as a file containing a URL, and whose filename is put in the Favorites list.”

“If the people who write the code do not feel responsible for the model, or don’t understand how to make the model work for an application, then the model has nothing to do with the software.”

“Every developer must be involved in some level of discussion about the model and have contact with domain experts.”

Discussion

  • How would this apply to a functional programming language (author examines procedural and logical paradigms, but not FP)?

    • Artifact of when it was written?

    • Would probably work fine.

    • Not as layered.

    • Examples in JS of functional composition (e.g. React).

    • Django's MVC setup actually makes it harder.

      • Not a place for a lot of domain logic (e.g. utils.py)

      • One strategy: Separate layer to proxy lower level models.

      • Another strategy: Service pattern, service encapsulates business logic.

      • Example of what's talked in the book about frameworks dictating a lot of the design.

      • Grades app does have classes to encapsulate domain, but have to go without using niceties like DRF serializers.

      • Question: Are some of the tools that make it easy to write code making it harder to separate out domain logic?

        • Do we need a layer of our own/special tooling to enable this?

  • Analogs between the Smart UI pattern and Django's "Smart Model" and its conveniences.

    • Django optimizing for building it fast? Easy to leak implementation.

    • Useful middle ground?

    • Enterprise API – package that's installed, but API calls are via REST API as if it were a separate system.

      • Business logic encapsulated in models.

    • How to handle internal Python APIs?

      • Best practice to not pass back models?

      • api.py is the public interface between Django apps (though we're not sure if we want to continue using it)

      • Pass back the model but only allow certain methods to be called?

      • Write business logic as functional modules and use the model methods just as a proxy to that?

    • What is the problem we're solving?

      • Enrollment example

      • Separating domain logic from underlying details

      • Simplifying testing

  • Question: Difference between Application Layer and Domain Layer?

    • Application layer manages jobs and tasks and lifecycle of the application. Very thin.

    • Layer for orchestration.

    • No business logic, just knowing who to call.

    • Initialization of the application, setup, teardown.

    • Django "best practice" as view, but this is often not followed.

      • Need to have a clear location for business logic.

    • In async tasks (grades example), is the thing that kicks off task the app layer and the task itself the domain layer?

      • tasks.py is the Django interface

      • domain logic is in the classes: CourseGrade, CourseGradeFactory, SubsectionGrade, and SubsectionGradeFactory

    • Where does app end and domain begin?

      • Where the framework ends and you create domain layer concepts and return domain layer concepts.

  • Question: Are permissions domain logic or app?

    • The notion of "readable by superusers" (app) vs. domain concept of who should be a superuser?

    • Attached to the View in our code.

    • Mapping of of roles to permissions, permissions to views in Django.

    • Permission a Guard on the view, and therefore a separate Domain?

    • Are permissions for what can be done in a Domain a separate Domain?

    • Implementation of permission is app, where to attach it is Domain.

      • Combination, since there's a lot of permission checking in the domain itself.

    • Artifact of the fact that it's a web app, because the conceptual Domain wouldn't even allow certain methods to be invoked? Different objects for different kinds of people?

    • Purer form of this might be to just pass permissions down to the domain layer and let it make the decisions.

    • When will we know that we've outgrown Django?

      • Many of the things we run into are fundamental to the framework.

      • Gradually outgrow it by pieces (asset pipeline, templates, etc.)

    • Django not strictly object oriented, applying separation of concerns without adhering too strictly to DDD?

      • In the end, it's just Python, we can shape it as we need to.

        • Easy to do in a small team, harder to do at large.

          • Especially at open source.

    • Workflow as business logic, app layer just doing I/O, piping to the next thing.

  • Monolith and Folder Structure: Anyone interested in figuring this out, contact @Nimisha Asthagiri (Deactivated)

    • First a template, then a script to generate that structure (manage.py – startapp at a specific URL already supported?)

  • Layers and directionality of layers

    • We should think about separation of communication and not just arrangement of app.

    • "You are talking to way too many places in the code."

      • Compositional components

    • Are we missing anything by having Django apps that cross all four layers?

      • Just a matter of discipline, would be confusing to separate out

      • Cross dependencies without directionality

      • Each Django app as its own Context

        • But part of a higher level tiering (different apps at different layers)

        • Middleware as an example of an app layer concern

          • But can be part of an app that spans all layers, e.g. language preference middleware that also has UI for users and model layer, etc.

            • How does this work for extraction, say pulling language related logic into a separate service?

            • App as glue, so app can get the language preference and pass it to the next thing that cares about language.

            • Common anti-pattern: Passing the request object instead of the things you need from it.

            • Service pattern, letting the Domain query from a service as it's needed.

            • Should we need to pass it down through the layers? Should things near the bottom need to know how to access services?

            • Pass in the service you need to use (for in-process services) to make testing easier

            • Don't like to pass the same parameters over and over again

            • Not opposed to passing the model (User instead of say six fields on the user)

              • If you're always sending the same parameters together, it's probably its own model

              • Difference between passing around simple structure vs. smart object that can make queries

              • Be transparent whether a call is a blocking call to an external service

        • Tracking and analytics are infrastructure layers

Chapter 5 A Model Expressed in Software

Quotes

Intro

Does an object represent something with continuity and identity - something that is tracked through different states or even across different implementations? Or is it an attribute that describes the state of something else? This is the basic distinction between an ENTITY and a VALUE OBJECT. Defining objects that clearly follow one pattern or the other makes the objects less ambiguous and lays out the path toward specific choices for robust design.

Then there are those aspects of the domain that are more clearly expressed as actions or operations, rather than as objects. Although it is a slight departure from object-oriented modeling tradition, it is often best to express these as SERVICES, rather than forcing responsibility for an operation onto some ENTITY or VALUE OBJECT. A SERVICE is something that is done for a client on request. They emerge .. when some activity is modeled that corresponds to something the software must do, but does not correspond with state.

Associations

There are at least three ways of making associations more tractable:

  1. Imposing a traversal direction

  2. Adding a qualifier, effectively reducing multiplicity

  3. Eliminating nonessential associations

It is important to constrain relationships as much as possible. A bidirectional association means that both objects can be understood only together. When application requirements do not call for traversal in both directions, adding a traversal direction reduces interdependence and simplifies the design. Understanding the domain may reveal a natural directional bias.

Constraining the traversal direction of a many-to-many association effectively reduces its implementation to one-to-many - a much easier design.

Entity

An object defined primarily by its identity is called an ENTITY.  ENTITIES have special modeling and design considerations. They have life cycles that can radically change their form and content, but a thread of continuity must be maintained. Their identities must be defined so that they can be effectively tracked. Their class definitions, responsibilities, attributes, and associations should revolve around who they are, rather than the particular attributes they carry. Even for ENTITIES that don't transform so radically or have such complicated life cycles, placing them in the semantic category leads to more lucid models and more robust implementations.

When an object is distinguished by its identity, rather than its attributes, make this primary to its definition in the model. Keep the class definition simple and focused on life cycle continuity and identity.

Value Objects

Software design is a constant battle with complexity. We must make distinctions so that special handling is applied only where necessary. VALUE OBJECTS are instantiated to represent elements of the design that we care about only for what they are, not who or which they are.

When you care only about the attributes of an element of the model, classify it as a VALUE OBJECT. Make it express the meaning of the attributes it conveys and give it related functionality. Treat the VALUE OBJECT as immutable. Don't give it any identity and avoid the design complexities necessary to maintain ENTITIES.

As long as a VALUE OBJECT is immutable, change management is simple - there isn't any change except full replacement. Immutable objects can be freely shared, as in the electrical outlet example. If garbage collection is reliable, deletion is just a matter of dropping all references.

Try to completely eliminate bidirectional associations between VALUE OBJECTS.

Services

A SERVICE is an operation offered as an interface that stands alone in the model, without encapsulating state, as ENTITIES and VALUE OBJECTS do. SERVICES are a common pattern in technical frameworks, but they can also apply in the domain layer.  They are intrinsically activities or actions, not things.

A good SERVICE has three characteristics.

  1. The operation relates to a domain concept that is not a natural part of an ENTITY or VALUE OBJECT.

  2. The interface is defined in terms of other elements of the domain model.

  3. The operation is stateless.

It takes care to distinguish SERVICES that belong to the domain layer from those of other layers, and to factor responsibilities to keep that distinction sharp.  Layers:

  • Application service

  • Domain service

  • Infrastructure service

Granularity

Medium-grained, stateless SERVICES can be easier to reuse in large systems because they encapsulate significant functionality behind a simple interface. Also, fine-grained objects can lead to inefficient messaging in a distributed system.

As previously discussed, fine-grained domain objects can contribute to knowledge leaks from the domain into the application layer, where the domain object's behavior is coordinated. The complexity of a highly detailed interaction ends up being handled in the application layer, allowing domain knowledge to creep into the application or user interface code, where it is lost from the domain layer

Modules (Packages)

It is a truism that there should be low coupling between MODULES and high cohesion within them. Explanations of coupling and cohesion tend to make them sound like technical metrics, to be judged mechanically based on the distributions of associations and interactions. Yet it isn't just code being divided into MODULES, but concepts. There is a limit to how many things a person can think about at once (hence low coupling). Incoherent fragments of ideas are as hard to understand as an undifferentiated soup of ideas (hence high cohesion).

..tiered architectures can fragment the implementation of the model objects. Some frameworks create tiers by spreading the responsibilities of a single domain object across multiple objects and then placing those objects in separate packages. At that point, viewing the various objects and mentally fitting them back together as a single conceptual ENTITY is just too much effort.

Unless there is a real intention to distribute code on different servers, keep all the code that implements a single conceptual object in the same MODULE, if not the same object.

Discussion

  • Entity vs Service discussion:

    • services are stateless

    • entities have an identification and state

  • Service examples:

    • Tracking service: Infrastructure level service

    • Grade service: Domain service

  • Event could be example of entity or value, depending on need.  Like address example in book.

  • Helpful to more explicitly look at the value objects and relationships.

  • Value objects: 

    • Emphasized immutability.

    • Is it just an Enum?  More like a struct.

    • Try to eliminate bi-directional relationships between values objects.

    • Can they do something?  Yes.

  • Opaque keys:

    • Value object themselves.  Intention to be immutable.

    • Used to identify Entities.

  • Granularity

    • Fine grained granularity leads to leakage.

    • Passing between layers, granularity makes a difference.

    • How to combat granularity?  Medium sized services to help combat.

    • Do we think this is true?  

      • REST APIs are difficult to use when too granular, 

        • Having endpoints that return everything, vs endpoints that return pieces at a time.

      • or

      • REST APIs with json.  GraphQL as example of making joins on the server.

        • Front-end vs Back-end knowledge of the needs of the front end.

    • Enrollments example:

      • Ask for all active enrollments of a student?  What are enrollments of this student with scheduling, etc.  Are the implementation details leaked out to the dashboard?  Want to avoid this.

  • Modules

    • Pitfalls of infrastructure driven packaging

    • Tiered architectures can fragment

    • Should tell the larger story of what is going on.

    • Should be defined in terms of the domain, not in terms of the infrastructure.

    • Important to not deprioritize refactoring to have your code match the model.

  • Ubiquitous Language

    • Can start small.

    • Do we want agreement to do for this?

    • Can be difficult when some people are strict and others don't care (past experience of some).

    • Refactor as you go.

Chapter 6  Life Cycle of a Domain Object

Quotes

Aggregates, Factories, Repositories

  • AGGREGATES tighten up the model itself by defining clear ownership and boundaries, avoiding a chaotic, tangled web of objects. This pattern is crucial to maintaining integrity in all phases of the life cycle.

  • using FACTORIES to create and reconstitute

  • REPOSITORIES address the middle and end of the life cycle, providing the means of finding and retrieving persistent objects while encapsulating the immense infrastructure involved.

Aggregates

Cluster the ENTITIES and VALUE OBJECTS into AGGREGATES and define boundaries around each. Choose one ENTITY to be the root of each AGGREGATE, and control all access to the objects inside the boundary through the root. Allow external objects to hold references to the root only. Transient references to internal members can be passed out for use within a single operation only. Because the root controls access, it cannot be blindsided by changes to the internals.

Example: denormalize price to satisfy Purchase Order Aggregate invariant.

Now, to translate that conceptual AGGREGATE into the implementation, we need a set of rules to apply to all transactions.
The root ENTITY has global identity and is ultimately responsible for checking invariants.
Root ENTITIES have global identity. ENTITIES inside the boundary have local identity, unique only within the AGGREGATE.
Nothing outside the AGGREGATE boundary can hold a reference to anything inside, except to the root ENTITY. The root ENTITY can hand references to the internal ENTITIES to other objects, but those objects can use them only transiently, and they may not hold on to the reference. The root may hand a copy of a VALUE OBJECT to another object, and it doesn't matter what happens to it, because it's just a VALUE and no longer will have any association with the AGGREGATE.

As a corollary to the previous rule, only AGGREGATE roots can be obtained directly with database queries. All other objects must be found by traversal of associations.
Objects within the AGGREGATE can hold references to other AGGREGATE roots.

A delete operation must remove everything within the AGGREGATE boundary at once. (With garbage collection, this is easy. Because there are no outside references to anything but the root, delete the root and everything else will be collected.)
When a change to any object within the AGGREGATE boundary is committed, all invariants of the whole AGGREGATE must be satisfied.

Factories

Creation of an object can be a major operation in itself, but complex assembly operations do not fit the responsibility of the created objects. Combining such responsibilities can produce ungainly designs that are hard to understand. Making the client direct construction muddies the design of the client, breaches encapsulation of the assembled object or AGGREGATE, and overly couples the client to the implementation of the created object.

The two basic requirements for any good FACTORY are:

  1. Each creation method is atomic and enforces all invariants of the created object or AGGREGATE. A FACTORY should only be able to produce an object in a consistent state.

  2. The FACTORY should be abstracted to the type desired, rather than the concrete class(es) created.

I refer to the creation of an instance from stored data as reconstitution.  A FACTORY used for reconstitution is very similar to one used for creation, with two major differences:

  1. An ENTITY FACTORY used for reconstitution does not assign a new tracking ID. To do so would lose the continuity with the object's previous incarnation.

  2. A FACTORY reconstituting an object will handle violation of an invariant differently.

Repositories

  1. Abstract the type of the object returned

  2. Take advantage of the decoupling from the client.

  3. Leave transaction control to the client.

The FACTORY makes new objects; the REPOSITORY finds old objects.
These two views can be reconciled by making the REPOSITORY delegate object creation to a FACTORY, which (in theory, though seldom in practice) could also be used to create objects from scratch.
One other case that drives people to combine FACTORY and REPOSITORY is the desire for find or create functionality, in which a client can describe an object it wants and, if no such object is found, will be given a newly created one. This function should be avoided.

A table row should contain an object, perhaps along with subsidiaries in an AGGREGATE. A foreign key in the table should translate to a reference to another ENTITY object. The necessity of sometimes deviating from this simple directness should not lead to total abandonment of the principle of simple mappings.

Questions

  1. I wonder why he recommends that the Repository be the primary interface for reconstituting objects - rather than letting the Factory be the primary.

Discussion

 

  • find_or_create

    • Client code shouldn't necessarily call it, however it's useful to have

    • Room opinion: Doesn't seem like it has to be a hard or fast rule

    • At the SQL layer, you have to have it to know whether you're violating a constraint

    • We use find_or_create primarily for Value Objects

      • Example: CSM state

  • django

    • is entity focused - not aggregate focused

    • has custom support for repository via models.py and ORM - and model managers

    • but does not have factory support innately

    • factories would allow creation of 

    • Model managers are similar to repositories

      • but you can pass any SQL queries - so easy to break abstractions

      • But then again, his example of custom queries was starting to cross the line

        • should this really belong at the domain-model layer?  Or should the domain layer only contain like 4 high-level methods?

  • It's Ok to go straight to constructor for small cases

  • Repositories

    • Allow us to replace the underlying database easily

    • You're supposed to know about the underlying implementation, but not interact with it directly

    • Maintaining an illusion that things are in-memory

      • @Dave Mohr (Unlicensed) says NO!!!  Because that can lead to scalability/performance issues

      • if read out of context, this could be misconstrued

      • In fact, this strongly argues for a Repository - since you can only include performant operations on the public interface

        • like exclude 'getAll' as an option to call

      • Sometimes overly hiding implementation implications from clients can lead to mis-use

      • Does the client need to implement an all-powerful error handling for all types of functions?