1

TL;DR: Should the domain layer span the entire breadth of the app such that everything can be simulated, from point A to point Z, or may the application layer be used as the orchestrator and serve as the glue between modular pieces of domain logic?


I read the first 100 or so pages of Eric Evans' book, where he describes his experience with the PCB project, simulating and testing the circuitry with plain Java classes, and raced to try it in my own project—with mixed results.

Said project is about exchanging writing prompts; a kind of "write this for me, and I'll write what you want in exchange".

  1. Users will type what they want to read into an input box very much like StackExchange's own, which will search for similar prompts, if any, or let the user create a new one.
  2. Armed with a prompt, the user will browse through other people's prompts, find those they wouldn't mind answering, and send exchange requests. (They can also leave their prompt "hanging around" and wait for exchange requests instead.)
  3. When the request is accepted, the users are given a deadline to submit a short story.

That's the gist of it. There are some business rules I omitted for brevity, some of which are shown in the event board below and others in the write-up at the bottom, and there are some details that need to be worked out and contradictions ironed out, but I figured this should be enough for a prototype.

event board

I went to work and tried to sketch out what the app might look like, but got stuck before I could really go anywhere.

pub struct Prompt {
    content: String,
    answers: Vec<Story>,
}

impl Prompt {
    pub fn new(content: String) -> Self {
        Self {
            content,
            answers: Vec::new(),
        }
    }

    pub fn demand_redo(&mut self) {
        for answer in &mut self.answers {
            answer.mark_as_unsatisfying();
        }

        // ...And then something?
    }
}

pub struct Story {
    content: String,
    score: u32,
}

impl Story {
    pub fn mark_as_satisfying(&mut self) {
        self.score += 1;
    }

    pub fn mark_as_unsatisfying(&mut self) {
        self.score -= 1;
    }
}

pub struct User {
    prompt: Prompt,
}

impl User {
    pub fn new(prompt: Prompt) -> Self {
        Self { prompt }
    }

    pub fn answer_prompt(&mut self, story: Story) {
        self.prompt.answers.push(story);
    }

    pub fn request_exchange_with(&self, _user: &Self) {
        todo!()
    }

    pub fn accept_exchange(&self) -> Exchange {
        todo!()
    }
}

pub struct Exchange {
    prompts: [Prompt; 2],
    // TODO: Replace with `DateTime`.
    deadline: (),
}

#[cfg(test)]
mod tests {
    use crate::{Prompt, User};

    #[test]
    fn integration_test_name_tbd() {
        // Arrange.
        let user_a = User::new(Prompt::new("scenario A".into()));
        let user_b = User::new(Prompt::new("scenario B".into()));

        // Act.
        user_a.request_exchange_with(&user_b);
        user_b.accept_exchange();

        // Assert?
    }
}

It's probably obvious from the code that I don't know what I'm even supposed to do. There are lots of questions I want to ask, but I have trouble even formulating them. Clearly, I'm doing something very wrong.

The main question is this: Should the domain layer span the entire breadth of the app? Should I be able to simulate everything that can happen? Or is it OK to use the application layer as the "glue", as the event system, and break down the domain logic into smaller, more modular parts?

Project Details

Scenario: Search

As a registered user, I browse other's prompts and find one I wouldn't mind answering. I send the author of that prompt a prompt exchange request.

Addendum: I may send the request to multiple authors.

Failure

If I'm unable to find a satisfying prompt, I can leave my own "hanging around", waiting to be picked by another. The website will send me an email and/or a notification when that happens.

Scenario: Collision

If someone else has already posted the same/similar prompt, and...

  • it has already been answered, I can simply read the answer.
    • If I think the story's OK, I'll just leave it be.
    • If I think it's great, I'll mark it as satisfying.
    • If I don't like it and want to reuse the prompt in search of a new answer, I'll mark it as unsatisfying.
  • it has not yet been answered, I can use the prompt as my own, effectively upvoting it.

This has several consequences:

  • When browsing prompts, they are sorted by their upvotes in descending order (combined with the promp author's own upvotes, reflecting their skill as a writer).
  • When I upvote a prompt, I subscribe to it, so I am notified when it's answered.
  • I can use another's prompt as my own and offer it in an exchange. That way, inactive prompt author's won't make me wait for them to come back online.
  • But I may pick a writer the prompt's author would not have, which is not fair to them. Therefore, they can still use their prompt in an exchange, thereby letting the prompt have multiple answers.

Scenario: Exchange

If that user accepts, we begin the exchange. We're given a deadline (likely chosen by the developer), and we must each write a short story (of a minimum length likely also chosen by the developer) answering the other's prompt.

Addendum: This requires I already have my own prompt in hand.

Scenario: Aftermath

The answer/story may also be upvoted—by the prompt's author but also other viewers. This score will be displayed on the profile of the story's author and will reflect their skill as a writer. Naturally, users will want to find the best writer they can to answer their prompt, so good writers will see more exchange requests and therefore have their own prompts answered more quickly.

4
  • "may the application layer be used as the orchestrator and serve as the glue between modular pieces of domain logic?" - yes! The domain layer needs to capture the knowledge of the domain rules, it doesn't need to work on it's own, in a self sufficient, self contained way. You're supposed to plug things in and around it. Otherwise you'll have mixed levels of abstraction, coupling, and instead of having components with well-defined jobs/responsibilities, you'll have magic black boxes that hopefully work, that you're afraid to touch because who knows what else might break. Commented Sep 8, 2023 at 20:17
  • @FilipMilovanović Yeah, I read ahead some more and got to the idea of services. However, the book did make a distinction between domain services, which handle all business logic, and application services, which coordinate domain objects and services with infrastructural and other layers. Commented Sep 9, 2023 at 3:23
  • Yeah. IIRC, the book presents different categories of domain objects (in terms of "tactical patterns", which are, mind you, optional and provided as an example of how one might go about putting DDD in practice), and then says that anything that doesn't fit neatly into one of those can be called a domain service. So it's not meant to be something of fundamental importance, but more of a general category. Commented Sep 9, 2023 at 9:57
  • But nothing really changes there: a domain service would deal with some business rule, something that's central to what the business does (e.g. if such and such data point exceeds some limit, return some indication that the limit has been exceeded), wheres an application service would deal with "orchestration", and "operational" things that could in principle change independently from the business rule - like, get the relevant data from the database (or, after an operational change, a web service, or an event queue, or...), call the domain logic and pass it a callback, etc. Commented Sep 9, 2023 at 10:01

1 Answer 1

4

Separating out what's "domain" or "business logic" from the rest of the application is not trivial or devoid of bias.

Especially when like here you arguably have CRUD operations on a DB and little else. and therefore no domain layer at all.

Is "User should have an "accept" button which they can click to accept a prompt" business logic, or presentation logic?

Is "Prompts must be able to be saved to the database" domain or infrastructure?

To an extent its down to you to decide what layer these belong in. But on the other hand, if you are being good and separating things out, it shouldn't matter too much what layer you consider any given part to be in. Just make sure all your references point the same way.

In this specific case I think the only thing you need in domain are your core objects, Prompt Story etc they don't need any logic.

Saving to the db you can have a repository, which would be in infrastructure

The rest, all the buttons you can click and views you can see; that's all just

get object
change object properties
save object

and is in application or presentation depending on if its got complex enough that you want to separate it out yet.

Now lets say you do all that and your are down the line a bit and want to implement:

"When a user scores a story, and they have already done so, they shouldn't be allowed to change their score"

Well now you have a choice of where to write that, Its good to imagine that you have more that one application for this purpose, say a mobile app and a web app, even if you don't. They you can ask yourself:

  • If I put this in the application I will have to write it twice!
  • If I put this in a api controller my controllers will be big
  • If I add it as an entity method, I will need to make DTOs for my entities
  • If I add it to the save on the repo, then the error will not be helpful

etc

Questions like these can help you decide where the best place for any given function is.

2
  • "Especially when like here you arguably have CRUD operations on a DB and therefore no domain layer at all." I was afraid of that. I picked this project because I felt it was simple enough to learn from but complicated enough to merit DDD. Do you think I should've gone with something meatier? I'm also puzzled by which entity to assign as the aggregate root: because a user's score is derived from their prompts and answers, it makes sense to go with the user entity, but from the "business" perspective, prompts are the core idea and users exist to support them, so it feels wrong. Commented Sep 9, 2023 at 3:27
  • DDD like other methodologies is more about project management of on going software projects in corporations rather than programming. So its hard to learn unless you are in that environment
    – Ewan
    Commented Sep 13, 2023 at 15:08

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.