0

BDD advocates for outside-in development because having a clear goal for your code helps you avoid getting bogged down in unnecessary details. It is usually mentioned in the same breath as DDD, but I'm having trouble reconciling the two. DDD urges you to build out your core domain independently of infrastructural concerns so you can focus on what matters most to your business, but how can you do that when outside-in tests will only pass after those concerns are implemented?

Let me give an example. I'm working on an app where writers can answer each other's prompts/premises. For the MVP, the basic workflow looks like this:

  1. Sign in.
  2. Create a prompt that you want someone else to write for you.
  3. Browse through other people's prompts in search of ones you want to answer and request an exchange.
  4. The other users will be alerted about the request, e.g. through email.
  5. The first to accept initiates an exchange, and now both users have until some deadline to writes short stories for each other.
  6. Submissions will be rewarded, while failure will be penalized.

To start with, I wrote an API test that covers Points 1 through 3. But even that much requires some form of storage for user accounts and prompts. I've got to implement the web layer and the persistence layer at the same time as my domain logic. At that point, why even bother separating the persistence mechanism from the domain layer using, say, abstract repositories, as DDD advises?

2 Answers 2

5

DDD urges you to build out your core domain independently of infrastructural concerns so you can focus on what matters most to your business, but how can you do that when outside-in tests will only pass after those concerns are implemented?

"Independently" does not mean "first".

The point of confusion here is unrelated to BDD/DDD. What it boils down to is the order in which you tackle things. That is a relatively subjective thing to do. Some people prefer to tackle things API-first and then flesh out the underlying logic. Other people prefer to start from a dependency-free domain which they will subsequently write an API layer for.

What BDD/DDD cares about is that your design of this particular layer is done for its own reasons, without forcing itself to comply with adjacent layers. Sure, if you tackle this layer before you build anything else, that can help you in making sure that you're not muddying the waters. But that's not a hard and fast requirement1. I'm experienced enough with domain-driven designs that even if I end up building e.g. the API or infra layer first, that I'm not going to end up compromising my domain's design due to my knowledge about these other layers.

Truth be told, I'm confident in my domain design because I have experience doing so. I'm not going to deny that when I just started out, designing the domain before I designed the other layers definitely helped keep me on the right track.
But it's very different to say that doing this layer first is a requirement versus it being helpful if you're lacking confidence or experience in your design. I agree with the latter, not the former.


I've got to implement the web layer and the persistence layer at the same time as my domain logic.

No you don't.

You have to write the persistence interface before you can obviously write the domain logic that interacts with said interface. But you don't have to bother with writing the persistence layer itself.
And even if you decide to do so anyway, you could get away with doing a slapdash in-memory persistence rather than consuming an actual dedicated persistence store.

Your assertion that you must develop these concurrently makes me worried that you are relying on end-to-end tests as the sole marker for progress, as opposed to having a test suite that is able to confirm individual layers are working as intended.

If that is the case, then I strongly suggest you dig into unit testing more than you currently are. I'm not saying you have to take it all the way to TDD, but I would suggest that you need to up your testing game to the point where you are able to reliably tell me if your domain/business layers are working without needing to involve any kind of persistence mechanism.

This response focused on persistence but the same point is true for the API. You should be able to confirm that your domain/business logic is working without relying on a real persistence store or API layer.


At that point, why even bother separating the persistence mechanism from the domain layer using, say, abstract repositories, as DDD advises?

Honestly? That question deserves an entire book in order to answer it, if not a book series.

I cannot explain this in its entirety in this answer, but I am going to point out that I strongly urge you to research what the purpose of abstraction is. If you think the only reason abstraction exists is to help you build something from scratch, then you have a significant amount of learning to do on this topic.

Succinctly, abstractions are an investment into your logic, which tend to come in the form of an added upfront cost when building things from scratch, but will pay back dividends when dealing with future maintenance of the codebase, as well as the ability to test components in isolation.

I put that last part in bold because this answer has already suggested that your approach to testing strategies (or lack thereof) is already a key contributor to the issue you're pointing out in your question. It would make sense that if you did not understand the purpose of abstractions and/or did not design them correctly, that you would find yourself having written code components that are nigh impossible to test in isolation without needing to write the full stack of layers before you can confirm any of your work.


1 On a bit of a tangent, this is my main gripe with TDD. It prescribes that you must write tests before you even consider building your logic. It's essentially what I said before: yes, doing that first will make sure that the first thing will not be tainted by the second thing. Whether it's about pure domain design or pure test design, it's the same argument in principle.

But the order in which you do things should not be the driving factor in how you design things. That's just silly. If you assert that a developer could not possibly design something if they already have knowledge of another layer, what does that mean if you realize after the fact that you have to revisit your domain/test implementation now that you've already started work on the other layers? Scrap the lot and start over? That would be nonsensical.

2
  • Thanks for the detailed answer. I think I'm indeed missing tests at the deeper layers. That said, I've read that you shouldn't mock your database—or any external service you have control over—and that goes double for an embedded database, like SQLite, which I'm using for this project. (It makes sense, too. What if there's an error in my SQL? I won't know until it's dispatched to the DB.) Is there still a point abstracting away persistence, then, when the interface will only ever have one concrete implementation? Commented Apr 15 at 3:07
  • 1
    @verified_tinker: Keep in mind that whenever you read advice, it might not apply to your case, or it might only apply in a limited subset of cases, or the person in question might simply have a different approach or opinion. There are many different kinds of tests and "you shouldn't mock your database" definitely does not universally apply to all of them. Whenever you ingest advice, always consider the frame in which it fits.
    – Flater
    Commented Apr 15 at 3:53
2

Recommended reading:

The underlying motivation for all of the ceremony is to drive down the costs of change. Abstraction gives us a mechanism for separating "important" details from "unimportant" details; by introducing abstractions, we reduce the amount of coupling to "unimportant" details, which reduces costs when those details need to change.

Your API, taken as a whole, is going to need to know about all of those important details. But not all of your app needs to know about all of the details.

In particular, your domain model does not need to be tightly coupled to the details of where input information comes from (or where output information goes).

As an example: the domain model typically does not care at all whether persisted information is "really" stored on disk, or not. If we introduce an abstraction that treats that detail as unimportant, and couple the domain model only to the abstraction, then we can replace our save-to-disk persistence strategy with an alternative strategy that only manipulates local memory, without needing to make further changes to the domain model.

And that, in turn, drives down the costs of conducting isolated experiments to measure the behavior of the domain model (controlling a private in memory data store is much easier than controlling a shared data store on disk).


Expressed another way: running controlled experiments on localized complexity via a remote boundary really sucks, because your experiments keep getting invalidated by changing details that are unrelated to what you are trying to measure.

Abstraction is one of the tools in the kit that allows us to move our experimental boundary much closer to the test logic, insulating the experiment from the effects of the unrelated changes.

2
  • "We can replace our save-to-disk persistence strategy with an alternative strategy that only manipulates local memory, without needing to make further changes to the domain model." What if I'm certain I won't need that? I'll only ever use one persistence mechanism—SQLite—which can live in memory; for tests. The persistence interfaces will only ever have one concrete implementation. In that case, does the abstraction not add extra complexity with no benefit? Commented Apr 15 at 3:18
  • @verified_tinker, say that again when you have 1000 tests cases that together run 10 SQL queries (so, each query gets exercised 100 times by the tests), but you can't speed up the test runs because your logic is too dependent on having an actual database that you cannot swap out sqllite for a fake database, leaving only a handful of test cases that run against the real database. Commented Apr 15 at 6:50

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.