4

Domain Object, while not necessarily a design pattern, can at least be considered as a design "concept", and one of its points is:

https://wiki.c2.com/?DomainObject

Domain objects should not direct their persistance. They don't know how to be persisted and even if they will be persisted. The infrastructure layer is there for that.

However, how far does this go in practice? Because to me, a straight up reading of "doesn't know how to be persisted and even if it will be persisted" means to me it should not be designed at all with persistence in mind. But the problem is, persistence fundamentally requires the ability to save the exact state of the internals of the object and then recreate that exact state when we want to load it again or, at least, that it must be able to save and load enough to return the object to a behaviorally equivalent internal state.

But suppose we have an object like this one. Originally I had a sort of vague example here, but now here is an exact, concrete one in Java language.

class RandomCircleMaker {
      // Takes a random number generator and a maximum radius.
      public RandomCircleMaker(IRng rng, float maxRadius) {
             mRng = rng;
             mMaxRadius = maxRadius;
      }

      // Pumps out Circle objects with a random radius between 0
      // and the maximum.
      public Circle makeCircle() {
             float radius = mRng.randomInRange(0.0f, mMaxRadius);
             return new Circle(radius);
      }

      private IRng mRng;
      private float mMaxRadius;
};

The question is, if I have something that then tries to persist this object, it at the very least has to be able to persist the maximum-radius field. Yet, it is quite clear that that field is private, as it should be to ensure encapsulation. Moreover, because the "spec" on which RandomCircleMaker was designed doesn't say anything about persistence, only that it should be able to be set up with a max radius and then pump out Circles with that fixed max radius, the designer naturally did not include a getMaxRadius method to get this parameter.

Hence, what happens when it turns out that we want persistence? I can only see a few possible options, none of which are ideal:

  1. In a language like the Java used above, it is possible to invade the internals of a class using reflection which, of course, obviously isn't something you generally should be doing, but persistence could be argued to be a special case. On the other hand, it means any modification of the domain object's internals requires a corresponding modification of the persistence layer (though not vice versa!). So we have a one-way tight coupling that, while natural, still "feels a bit too tight".

  2. Regardless of that, not all languages permit such a move. In C++, for example, we don't have such a thing as reflection. In that case, it seems the only way we can get around that is to add a method like getMaxRadius, or else make "friends" with the persistence classes - but does that count as "knowledge" of persistence for the sake of the Domain Object concept, even if it isn't strictly a persistence-related method like including an explicit "save" and/or "load" method? Or is this, much like reflection itself, an acceptable "break" from this particular design concept's sub-principle? If it is knowledge and also not acceptable, how on earth is one supposed to persist the above object?

  3. Because the maxRadius parameter does have impact on the circles generated, the persistence system could perhaps try to guess it statistically by generating zillions of circles. The problem here is this is not exact persistence, it has a chance of failing and it has a chance of taking an extremely long time to come up with a good result. Not to mention that it depends on assumptions about the implementation that may not be in the contract, such as the probability distribution for the particular random generator passed (which would technically depend on the contract for IRng, i.e. is that randomInRange meant only to give a uniform distribution, or can it give, say, a Gaussian, or ...?).

Hence, it seems I am missing something fundamentally important about what has been said on the C2 wiki - and if so, what is it?


Note 1: I suppose in C++'s C subset one can try to memcpy and that at least produces a binary copy while being at least as "rude" as reflection, but it's even more dangerous than reflection due to the fact of raw memory access potentially inviting a security hazard, and last of all, you are pretty much stuck with a very opaque binary image only - no, say, writing to JSON, XML, any kind of queryable RDB like SQL, or other more convenient, transparent, and/or interesting or structured formats. Not to mention being completely and utterly non-portable [fire that stuff up after saving from x86 on an ARM processor, and it almost surely goes boom-boom].

Note 2: I suppose one could work around that in C++-like languages by writing one's own code generator or pre-processor (think Qt MOC) and adding it to the build chain, that would recognize non-standard extensions to annotate the persistence objects (say "#persists RandomCircleMaker"), strip them and go over and insert the friend declarations before passing to the compiler so that at least in the directly human-touched code there is no "knowledge", but that seems like a heck of a lot of effort in the name of this "principle". And I've never heard of anyone doing that before, though of course that isn't saying much.

1
  • 5
    The classic OOP Design Pattern solution to this problem would be the memento pattern. In practice, it is more common to make reflection a core concern of the domain object (either using language features, or by some kind of visitor pattern), allowing the domain object to be serialized without depending on a concrete serialization/database format. Domain Driven Design notes that databases and object models are intertwined, suggesting to encapsulate DB operations in a Repository that has full access to your entities (compare C++ friend).
    – amon
    Commented Jun 18, 2023 at 6:25

9 Answers 9

9

Does a Domain Object being "completely ignorant of persistence" conflict with it always being possible to persist?

It conflicts with the domain object managing its persistence. It does not conflict with the domain object spiting out a DTO (data transfer object) that can be used to preserve its state.

Persistence can happen a few different ways:

  1. A domain object can intimately know how to persist itself when told to save (to file, DB, etc)
  2. It can be a pile of public fields that some persistence scheme can read
  3. It can spit out a data structure (DTO/Collection/String) that can be used to save or restore its state

Being "completely ignorant of persistence" says not to do 1. Encapsulation says not to do 2. Which leaves us with 3. The Memento Pattern is one way to do 3. Another way is Event Sourcing where a state can be represented as a collection of the events that lead to that state. Hell, toString() is a way to do 3. Just not a very reliable one. There are many state managing patterns that do a lot more than persistence. There’s also undo, rollback, and some third thing I can’t think of at the moment.

The real point here is externalizing state in some way is still ignorance of persistence. The domain object doesn't even know why it's externalizing state. With external state you can add whatever persistence you like without the domain object knowing about it.

1
  • In some application, we implemented copy/paste and it turned out the class specific code for that allowed persisting. (Long before I ever heard of any design patterns).
    – gnasher729
    Commented Jul 18, 2023 at 20:52
5

to me, a straight up reading of "doesn't know how to be persisted and even if it will be persisted" means to me it should not be designed at all with persistence in mind.

Though I understand how you would get to that conclusion, one doesn't follow from the other - in fact, I'd say the opposite: you explicitly design your domain objects so that they don't have to know how and when they are to be persisted. I.e. if you know you are going to persist your domain objects, they will be designed so that you can, even though the domain objects themselves still will not know how or when.
The "memento" pattern has been mentioned - a very good way of dealing with it - just enough information to be able to save and restore a (domain) object exactly with no additional responsibilities. The persistence framework can deal with the memento object in whatever way it likes - flat file, XML, JSON, custom binary data, a database... as long as the memento can be persisted correctly, so can the domain object.

7
  • 1
    Whilst I accept that individual objects do not have to contain the exact details of how they are saved, the design of the primary objects in an object model are often capable of exerting constraints on the matter of when or how the model can be saved. Also, if an object takes responsibility for yielding a memento object, then it effectively has taken responsibility for its own saving - or if the memento is generated externally, then you're back to the issue that the internals of the primary object must at least be accessible externally.
    – Steve
    Commented Jun 18, 2023 at 11:36
  • 6
    +1, I think the OP interprets this discussion from a W2 wiki (which contains only a few terse statements!) way too narrow. "Persistence ignorance" does not mean to design a domain model without persistence in mind, quite the opposite.
    – Doc Brown
    Commented Jun 18, 2023 at 13:58
  • 1
    @The_Sympathizer: the one given in this answer.
    – Doc Brown
    Commented Jun 19, 2023 at 21:20
  • 1
    @The_Sympathizer: I am talking about "you explicitly design your domain objects so that they don't have to know how and when they are to be persisted." - note that "if it will be persisted" from the W2 wiki was exchanged by "when it will be persisted". And according to your question about "memento": that sounds like a good new question for the site.
    – Doc Brown
    Commented Jun 20, 2023 at 5:34
  • 1
    @The_Sympathizer, on the memento, I don't think you could assume it is "supposed to be opaque". But you're right think it may be opaque (depending on the intended use), and therefore the only guarantee it will be transparent and compatible with an external persistence layer, is if someone designs it to be transparent and compatible with the external persistence layer in the first place. The existence of any controls on access to internal state, makes the design of the persistence method indivisible from the design of the primary objects - the access controls and persistence must cooperate.
    – Steve
    Commented Jun 20, 2023 at 10:29
2

The core conflict in your question is the consideration of who drives the design.

it should not be designed at all with persistence in mind

What this means is that the domain model's design does not entail any persistence considerations.

I'm aware that you probably already know this, I'm mentioning this to contrast it against what I'm going to say next.

But the problem is, persistence fundamentally requires the ability to save the exact state of the internals of the object and then recreate that exact state when we want to load it again.

Correct. The important thing here is that the design on how to do so is done from the perspective of the persistence layer, and it should assume that the domain model's structure is set in stone and non-negotiable.

That means that there must be some way for the persistence code to get into the internals of the object and extract the relevant information.

Most of the time, the publically accessible domain data will suffice in order to recreate an item.

In essence, my persistence machine will record the animal's shape, sound and its gait. When asked, it will then generate a sentient blob in the recorded shape, that makes the recorded sound and has the recorded gait.

If that sentient blob looks like a duck, walks like a duck and talks like a duck; for all intents and purposes it's a duck.
I apologize for the long setup for this weak pun.

If, however, there are some secret internals that mean that you cannot define your animal purely by its shape, sound and gait; then you need something that is able to deconstruct the animal into individually visible parts.

More often than not, this is done through simple division into subclasses, therefore rendering each class individually able to be restored by its public data.

If that still doesn't work, I first urge you to reconsider your domain model, it sounds horribly complex.
A domain does not care about access control, so I would expect that your model would inherently already always be broken into publically accessible (within the domain) parts that define themselves clearly enough. I'm only continuing this answer in the assumption that there is some freak edge case that I've not yet considered.

Assuming you haven't backed out at this point, then you're clearly dealing with a model that is complex enough to warrant having a component that deconstructs your domain model into cleaner parts.

Now if I have something like a, say, MyObjectPersister that tries to persist this object, it has to somehow be able to access the internals

I disagree. The persistence layer should only be interested in persisting that which you give it. Therefore, the persistence layer should not be bothered by deconstructing your input. Instead, you should feed already deconstructed parts into your persistence service.

The persistence layer should not care about intricate model structures and how they relate to one another. That is inherently a domain concern. The persistence layer should be a much dumber "I receive [thing], I store [thing], I retrieve [thing]".

Because I like analogies, a coat check attendant need never be made aware of which pieces of clothing your outfit consists of and what should/n't be stored in the coat check room. You simply give them your coat to store, and you give them your hat to store, as individual operations. You don't walk in and let the attendant decide for themselves what clothing to take off of you, nor do they dress you when you wish to retrieve your items.

to invade the internals of a class using reflection which, of course, obviously isn't something you generally should be doing, but persistence is a special case where it is very natural, intuitive, and clean, and it's impossible to decouple a data model from changes in what you are going to be saving because you have to, well, save whatever changed things you have correctly, too.

Hard disagree. Reflection is a kind of cheat code that tends to shut out your architectural design and access control. It exists for very specific purposes, but a design should never be built on the assumption that reflection will be used, even if the language clearly features it.

There are cases where reflection makes sense as a design decision but they're decidedly more complex and pioneering than the basics of persistence, which is what this question is about.

Or is this, much like reflection itself, an acceptable "break" from this particular design concept's sub-principle?

The point of domain-first design is that the domain shapes itself without needing to kowtow to external considerations such as persistence. By using reflection, you are putting expectations in your persistence logic which rely on structures that have intentionally been hidden from sight for anyone outside of the domain.

If you were to work with an N-tier architecture, however, things change. Here, persistence is the bottom-most layer, and it has the authority on how it designs its entities. This is a persistence-first design.
The subsequent consumer (your "domain" although it's mostly called the Application layer) will have to map the persistence entity to its own application model.

The responsibilities are different in this model. Here, the persistence layer has all the freedom to define itself and it's up to the application layer to kowtow to its design. This is generally not how developers set their priorities, which is why domain-first has become significantly more prevalent than it used to be.


There is one more edge case here, and it is one that I've historically seen architects push back against but I am strongly in favor of (not as a blanket solution but as a valid option)

We once dealt with a codebase whose domain was justifiably very complex, and needing to expose the internals would pollute the cleaner public interface. We retained the inverted dependency between domain and persistence, but we integrated the persistence interfaces into the domain. Effectively, the domain designed the persistence interfaces1. The persistence layer only provided implementations of said interfaces.

The core outcome here, which is relevant to you: The application layer told the domain "hey I want to store this [aggregate]", and the domain would tell the persistence layer what to store. This way, the domain retained knowledge of the private parts of its model, but it exposed very clean interfaces for persistence to work with.

  • The interfaces would be straightforward repositories. Using your example, it would be a IMyObjectRepository, IDependencyARepository and IDependencyBRepository.
  • The persistence layer simply implemented three individually simple repositories. It did not need to know about how these things related to one another.
  • The application layer would only work with MyObject directly, and it would use myObject.Save() to trigger persistence.
  • The domain model would implement that Save() method in a way that it would end up calling the IMyObjectRepository, IDependencyARepository and IDependencyBRepository.2

Yes, this involves the idea of persistence with the domain. However, this was considered an obvious side effect of designing a domain with such a need for privacy that the domain would not be designed in a way that an outside observer would be able to persist and reproduce these very complex objects.

One of these has to give. Either you design your domain in a way that an outside observer can persist it, or you allow your domain to involve itself with calling the shots on persistence (in the cleanest way possible, without any technical implementation).


1 This is not to say that the domain designed the entities! The interfaces either reused the domain models or a new DTO (if it couldn't be avoided). If the persistence layer needed a different kind of entity because of some technology-specific consideration, then the persistence layer would map the incoming object onto the entity that it preferred to work with.
In essence, the persistence interface did not use persistence entities as its in/output. The persistence layer was given the freedom to decide for itself whether or not it would reuse the interface models as its entities, or if it preferred to design its own entities and map one to the other.


2 For our particular case, this was a very complex process and not just a straight up call to the repository, but that consideration is not relevant for every use case. How you implement that save logic is up to you, but the core effect from it should be that your logic ends up calling the repository interfaces in the way that it needs to.

1
1

Your interpretation of the C2 wiki piece is too narrow. Just because you were slightly tweaked for someone else to persist you, doesn't mean you know about persistence.

"Know" here refers to behaviour.

Know how to be persisted means to have some behaviour that knowingly participates in persistence.

Know if they will be persisted means to have behaviour that is aware and relies on whether we were or were not persisted.

Moreover, and this is a separate question, yes, technology - language, framework, etc. - affects a lot how easy it is to have someone else persist you while maintaining encapsulation. It is one of the many cases where technology makes software engineering patterns possible.

2
  • This seems the most cogent and practically useful answer. +1. So the trick would be then to ask, basically, "what method could we add that doesn't 'break the spirit' of the encapsulation" - in this case, adding a "getMaxRadius()" method is perfectly fine, because external users already can surmise that somehow that parameter must be "in" there somehow (even if not explicitly as a variable called that, for whatever reason). Commented Jun 27, 2023 at 15:15
  • 1
    Yes. Note that the Memento pattern mentioned in other answers remains a good choice if you have many of these fields (or other hurdles your language/persistence framework cannot pass without unacceptably dirty hacks). Commented Jun 27, 2023 at 15:37
0

In some environments, you'll have a Domain model, in which objects live, perhaps a graph of inter-related object, and then also, a persistence model carefully designed with (say, relational) properties like various degrees of normalization, and the capability of answering a number of different kinds of queries with acceptable performance, perhaps even handling heavy transactional workloads along side.

If you have such an environment, then persistence is not necessarily merely flattening the state of domain objects, but rather capturing entities and their changes, perhaps with a transactional interaction.  Thus, there it is not necessarily the domain object's job to self-flatten, and instead this may be handled by mapping infrastructure that understands both the domain object models and the entity persistence models.

In any case, there often needs to be some handling of objects with mutual cross references, so that introduces some concept of persistence with a view toward the larger data set than a single object has of itself, along with some handling of transactional grouping of changes across multiple objects, e.g. commits & rollbacks in case of errors.

6
  • 1
    I'm not sure how this answers the question. A more complex database model means we may need all the more to get at the internals of the objects so it can "cut up" and represent their state in the most useful possible way. The point is that in my example the internal state of "MyObject" is completely opaque to "normal" code after construction. Commented Jun 18, 2023 at 1:55
  • @The_Sympathizer looks like you are assuming that DDD makes everything opaque, but it doesn't. DDD encapsulates, not obfuscates. Even if it does, some patterns like Unit Of Work give a space for you to change DDD components, track these changes and persist these changes w.o even mapping models.
    – Laiv
    Commented Jun 19, 2023 at 6:53
  • @Laiv, I don't think his argument is that DDD requires things to be opaque. It's that any kind of persistence requires state not to be opaque at all, but to be fully transparent and settable. Therefore, persistable objects are subject to a constraint that either they must be designed not to hide state, or if they do, they must be designed to at least allow transparent access for a persistence purpose (even if they are opaque for other purposes). This logically means objects have to be designed to cooperate with their own persistence - they cannot be designed "in ignorance" of persistence.
    – Steve
    Commented Jun 19, 2023 at 19:22
  • They can. That's the thing. The problem is that no one wants to pay the cost (which can be high)
    – Laiv
    Commented Jun 19, 2023 at 21:57
  • @Laiv: I suppose, e.g. you could indeed do what I mentioned in my post where you run "doSomethingCool()" over and over to increment the counter appropriately (and/or perhaps have something in persistence that runs it once to try and "feel out" where the counter is or something like that). Which would be both computationally expensive, and perhaps also coding-effort expensive when it comes to making that "figure-it-out" engine. The question is though, is "not paying that cost" really a "problem", then? It would have to only be if the benefit outweighs the cost to pay. (...) Commented Jun 19, 2023 at 23:52
0

Your object shouldn’t be designed with persistence in mind, but if I want to persist it and restore it, and your interface and code doesn’t allow this, then there’s a bug in your code, and you are going to fix it. That’s all there is to it. Or do you think your team leader will tell his boss “unfortunately our end users can’t save their documents because instances of class X prevent persisting”?

In some cases you can away with very little: Have setters and getters for properties that allow my persistence code to get all the data I need to persist an object and restore it. Most likely you have those anyway. If your setters send notifications when changing properties then have a call to creat instances where this is initially turned off and only turned on when the instance is restored. If you have a property for “creation date” and “last modification date” then have properties so I can persist them and restore them as the last thing after restoring an instance (persisting and restoring should leave those unchanged).

0

I've been thinking about this for some time now and have decided to revisit it, which means I'm now going to take a shot at answering my own question.

I will say that yes, in an ideal world, "persistence ignorance" means exactly what it says, in the 100% literal sense, which is most clearly articultaed by what @Flater said in the chat as the "standard software engineer interpretation": don't necessarily think of the code per se, but instead think of the code as combined with a dedicated human (and perhaps sometime, AI) maintainer. The "code knows" means "the code's maintainer knows" and this maintainer should be able to be completely oblivious to that anyone else is doing anything regarding persistence. And yes, the logical conclusion of that is that if they have to even so much as add a getter or setter to the class, then that is breaking the principle.

So how does that answer the question? Well, logic is king, and logic thus compels us to conclude there are only two possible things we can do when we encounter a "tricky" situation like the one mentioned here:

  1. The first way is that the designer of persistence contorts the persistence layer in whatever way is needed to save the state. Fundamentally, in the case of our RandomCircleMaker as designed, this means we have to regard RandomCircleMaker as unpersistible. Even if we want to persist it, we can't. As a result, we have to design around the fact it is impossible to persist. This likely means that we do something else, such as capture the radius the user entered separately into something we can persist. One of the most usefully domain-oriented constructs would be a UserSettings object that just stores a bunch of key/value pairs, since most reasonable applications one can imagine have a natural "user settings" concept.

  2. The alternative, if for some reason this doesn't work out, such as the object containing internal dependencies passed by its constructor that may have state also in need of persistence (pretty much ubiquitous if you do a composition-over-inheritance design) is to compromise on the ignorance in the smallest possible way. That is to say, we can allow adding new getters and setters to the object that are just enough to permit capturing and rebuilding it in at least a behaviorally-equivalent state. Having a method and a constructor that emit and consume a Memento-like object are a potentially good way to do this (note that passing the memento via the constructor prevents abusing it to arbitrarily change the state of a live object).

There are no other alternatives. Either the maintainer knows "at least a little", i.e. they have to lift a finger at least once in a way causally connected to the presence of persistence, or they know nothing. Logic trumps ideology every time.

Nonetheless, given that the ideal still is zero knowledge, I've started to think that perhaps the problem "really" lies in this case not so much with the coder(s), but instead with many common programming languages. The problem is that the knowledge relationship that we want to model is an asymmetric one: "A does not know of B but B knows of A", where "A" is a domain object and "B" is a persistence system. However, the most common object-oriented coding languages like C++ and Java only provide the classic "public/private" model, which is a symmetric model: either everyone knows about something, or nobody does except the thing containing it itself.

In this regard, there is no "shame" in compromising the principle to the minimum extent required when that is actually necessary, because you are in a sense having your hand forced by the language. Keep in mind those "popular" languages were created prior to there being as much collective experience with software design as now. So I may suggest to those designing tomorrow's languages that it might be a good idea to wither public and private!

-1

"the problem is, persistence fundamentally requires the ability to save the exact state of the internals of the object"

Stop making stuff private, embrace ADMs.

DDD is fundamentally about making your code match the way the business talks about their concepts. 'Normal' people don't understand "We can't do that because that information is encapsulated in a class". Or "but CustomerBase doesn't implement Buy()!" The idea behind making your code match the business terms is so that you never have to say that kind of thing to them.

If your Domain Object is Customer or example it should have public Name, Address, etc etc writing Database.Add(Customer c) is not a hard problem.

If you have a complex process that merits OOP, put it in a Domain Service where you can get as freaky as you like. Don't have Customer.SendMarketingCampaignBasedOnMachineLearningDerivedPreferenceChristmasDeals() have AiMarketingCampainService_2023.Run(List<Customer> customers)

1
  • 2
    If it needs persisting then it’s not internal state.
    – gnasher729
    Commented Jun 22, 2023 at 16:47
-2

The question is not entirely clear, and is written in jargonistic style such as talking of whether an object "has knowledge of" something.

It seems to me that what you're really asking is whether you, as the program designer, have to design the primary objects in such a way as to be amenable to being saved (and reloaded).

Or putting the same question conversely, whether you can design the primary objects without regard to saving and reloading at all, and yet still expect saving and reloading functionality to work later when you do in fact fit that part of the design.

You're not talking about the knowledge of the object. You're talking about your knowledge, and what things you need to pay attention to as a designer whilst you are designing.

At the level of the object model, and assuming working only within it's explicit interfaces and access constraints, the answer to the question is no: you cannot save and reload an object model, without first designing it to be savable and reloadable.

The essential requirement is that all the state of the object model must be accessible to an algorithm for saving, and that for reloading it must be possible to generate objects and put them into a particular state (as defined by the reloading algorithm) without reactivity from the object (at least not so long as the model remains in a part-built condition).

Because it is possible to design object models that don't permit this kind of access through the usual interfaces of the object model, it follows that the object model must be designed to allow it (at least implicitly). Whether that is through friend access, or special interfaces and methods, or whatever, the point is the access must be available.

An alternative is to violate the conventional constraints of an object-oriented model, by using reflection or direct memory access. In this case, the object model does not logically need to be designed to allow saving and reloading, since this will occur later using methods that don't conform to the patterns of object-oriented programming, and aren't bound by its constraints or internal workings.

However, even in this case, it may be desirable to design the object model with regard to the fact that it will be saved and reloaded by these means, because if not then the amount of complexity in the saving and reloading algorithms may be much greater.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.