2

I am trying to understand layered architectures and apply some DDD fundamentals to improve my architecture. I am finding that determining "what" an application service actually is, what its bounds are and what questions it should answer is really hard. Similarly for domain services.

Overall, I can't quite comprehend the scope of these things. Now, I understand there are so many resources on this and each resource seems to do application services slightly differently, and I have read a lot of them. I still find it very difficult to break down a problem into correctly sized scopes. I especially get confused when I have to use classes with these services. There doesn't seem (to me) to be a logical class grouping for them.

I have a made up use-case that I want to describe my thought process on what belongs where without code. I am hoping that some feedback on if I am thinking correctly or incorrectly can be given

Use case

A user wishes to click a button on a website to generate a random animal. Some additional information about the animal should be displayed with the generated animal. The set of animal species to choose from will come from an external API. The same external API provides all required additional information about the animal.

Domain

The domain will likely have to have an AnimalSpecies entity, which will contain an ScientificName and CommonName value object and any additional information that is useful for the domain. The usecase only describes the species, but if we imagine it contains things like common colours, average height, etc, then perhaps these are capturedin an AnimalCharacteristics value object. I suppose that common colours could be an iterable of Colour value objects, average height may need to be a Height value object to ensure the Height is > 0, and it may contain a range of expected heights.

Now I have described this using DDD entities and value objects, but with those descriptions it seems that AnimalSpecies should also be a value object. An AnimalSpecies would be equal if the values are equal, but at the same time you can identify a species by scientific name. In DDD, you supposedly can't create repositories for value objects, thus making it impossible retrieve them from an external API.

If I follow the repository pattern, I suppose I would have an AnimalSpeciesRepository which would expose a method to get a single species by scientific name or get all animal species. AnimalSpecies get(scientificName) and Iterable<AnimalSpecies> getAll(). I am unsure if it should have an api for AnimalSpecies getRandom(), because it's costly to sample an iterable. The implementation for this in the infrastructure layer would be where the external API is connected. I suppose the pagination required for this would be baked into the returned iterable.

Now a random animal could be created by getting a random AnimalSpecies and selecting a value from all the ranges given in the AnimalCharacteristics of the species. I believe Animal to be an entity, because 2 animals aren't the same because they look the same. It seems like Animal should have its own AnimalCharacteristics but the information wouldn't translate correctly. EG Height shouldn't be a range. In fact, it may make sense for an Animal to contain its AnimalSpecies. So, for now the Animal has an AnimalSpecies and AnimalSpecifics.

To generate a random animal, I suppose the actual logic would be in a domain service. An animal or its species really doesn't have knowledge of how to generate itself. So, Animal createRandomAnimal(AnimalSpecies). I dont know if this belongs in a class, nor the class it would belong to. But essentially, take the species, select values from with the ranges of the species.

Application

I believe here we would have a function like Animal generateRandomAnimal(). This would need the domain service and repository described above to generate a random animal. Its honestly simple enough of a use-case with all the ground work set to just write it

Animal generateRandomAnimal() {
    AnimalSpecies randomSpecies = speciesRepo.getRandom()
    Animal randomAnimal = animalService.createRandomAnimal(randomSpecies)
    return randomAnimal
}

Now, this must belong to a class to have access to the speciesRepo and animalService (name given for the domain services), else it will have to be injected to the function which sucks. I honestly have no idea how to name it.

This is my best attempt to describe the architecture, and I would love to be roasted in the answers.

6
  • I think if we consider reducing the API of your service method to animalService.createRandomAnimal() the answer to your question will present itself. Aug 3 at 2:47
  • @king-side-slide So you are implying that I should use the repository in the domain service instead? I'm not sure I see the benefit in doing this. That would restrict me to the species within the repo, but it might be realistic that I want to provide the user some ability to specify their own species and use it to create a random animal. It seems that the selection of a random species should be separate from the creation of a random animal based on the species selected. Aug 3 at 6:44
  • > It seems that the selection of a random species should be separate from the creation of a random animal based on the species selected. Why? Any consumer of your generateRandomAnimal method is already expecting an interface where they provide no input and produce an Animal. Why does generateRandomAnimal even need to exist? Move all of the logic into your service. Problem solved. Aug 4 at 2:42
  • @king-side-slide I create small and focused functions for the purpose of reuse and testability. I can far more easily test what is essentially a factory method in createRandomAnimal if I provide a species rather than expecting it to select for me. Further, lets imagine a new use-case which requires an application level function generateAnimalFromSpecies. If I put the repository access into createRandomAnimal I can't reuse it without changing it. I guess YAGNI applies but principles dictate separation of responsibilities to me. Aug 4 at 3:55
  • You misunderstand. With the exception of including UI related logic, I believe @Robert Bräutigam has the correct solution (and conceptual framework behind it). There are still 2 small focused functions. They are just located in different places: animalService.createRandomAnimal() and Species.generateRandomAnimal() Aug 6 at 14:37

2 Answers 2

3

I am finding that determining "what" an application service actually is, what its bounds are and what questions it should answer is really hard.

It's because they're not a real thing, they're not part of the "domain". They are technical artifacts, pointing to a design problem or mistake. Eric Evans in the Blue Book writes "Sometimes it's just isn't a thing" (or something to that effect) as a tagline to the service chapter.

As David West writes in "Object Thinking", if you properly model with objects, there should be nothing left afterwards. There shouldn't be "services" left dangling outside of your design.

The domain will likely have to have an AnimalSpecies entity, which will contain an ScientificName and CommonName value object and any additional information that is useful for the domain.

The usecase only describes the species, but if we imagine it contains things like common colours, average height, etc, then perhaps these are capturedin an AnimalCharacteristics value object.

This is where the problem lies. Objects are not defined by things they contain. In fact the opposite is true, what they contain is irrelevant. How they behave is everything. Objects do things, instead of have things.

Even entities and value objects. You can add() Money "value objects", or fire() Employee entities, etc. It's not important at all what data these contain.

Use-Case

Ok, so how do we model what you've described? From what I understand here are the requirements:

  1. User selects species from a provided list in UI.
  2. A random animal is generated for that species and shown in the UI.

Let's try to model that.

public final class Species {
   public InputComponent<AnimalSpecies> displaySelector() {
      ...
   }
}

public final class AnimalSpecies {
   public Animal generateRandom() {
      ...
   }
}

public final class Animal {
   public Component display() {
      ...
   }
}

For the use-cases that you've described this would be the model. You can read what everything does easily, it reflects what is required precisely and it doesn't matter what data each have at all.

What is missing is the Page on which the user uses all these things. That binds all these things together and there is nothing else that needs to exist.

Also notice that all the public symbols in those objects are domain-related, i.e. from the ubiquitous language. All those words are derived from the two lines of requirements above. None of those words are foreign or technical in nature. This is, I think, extremely important.

Even if the use-case is different, or I understood them wrong, the general idea I hope is clear.

Also note, that it doesn't matter one bit whether technically Animal is an entity or value object, whether Species is a "service" by some definition. Things are what they are because of their behavior, not by "nature". If you move behavior around, the "classification" could change. It doesn't really matter, does it?

0
2

Now I have described this using DDD entities and value objects, but with those descriptions it seems that AnimalSpecies should also be a value object. An AnimalSpecies would be equal if the values are equal, but at the same time you can identify a species by scientific name. In DDD, you supposedly can't create repositories for value objects, thus making it impossible retrieve them from an external API.

Reading this paragraph, I would like to address a few claims that you made. First up, AnimalSpecies being an entity. As you pointed out, this object has an identifiable field, its scientific name. However, I would argue that your analysis should not stop there. The other distinction between Entity and ValueObject is their lifecycle. Entities have lifecycle, they are mutable, their state changes as users interact with them. ValueObjects, on the other hand, remain the same. Now, within your own bounded context, which deals with generating random Animals, an AnimalSpecies object would never change based on user input. Your service does not own the logic behind its changes, this is governed by the external API you would be using. This raises another question, then. If the external API changes an AnimalSpecies object, how does this affect your service? From the simple use case you explained, you do not store the randomly generated Animals. Indeed, your service would not even be aware of a change in an AnimalSpecies object until the next time you receive this object from the external API, thus treating it as an entirely new object. Following this line of reasoning, your Animal object, in your own bounded context, similarly, does not have a lifecycle. Existing instances of Animal would never change, under the use case you described.

My point is, just because a term as general as Animal may behave as an Entity "in the real world", it does not mean it is one within your own bounded context. You are dealing with a very specific use case for an Animal, and indeed, the concept of Animal in your bounded context may be different from the concept of Animal in another application, for example a vet clinic.

The next point I would like to address is having a repository for a value object. The requirement to persist an object does not immediately make it an Entity. If an object is never to be updated, merely created and stored, then I would argue the requirement for persistence does not automatically promote it to an Entity. In your case, the situation is even simpler, since, you would not even create nor store AnimalSpecies from the repository. It is in fact a read-only repository. There are plenty of valid reasons why someone would want a repository to return a ValueObject, for example calculating a total sum, or aggregating through a set of persisted objects. These operations would return a result of their calculation, which is immutable.

Continuing on, I would break down your domain into the following responsibilities:

  • Somehow sample a randomized instance of AnimalSpecies
  • Construct an Animal from a list of AnimalSpecies

You could make the argument for three responsibilities, such as:

  • Retrieve all AnimalSpecies
  • Sample a random list out of a list of AnimalSpecies
  • Construct an Animal from a list of AnimalSpecies

However, retrieving all AnimalSpecies might be an expensive operation, so it may be best to abstract this part away, so as to allow for more efficient implementations.

Then, you would have a domain service to give you a randomized list of AnimalSpecies, regardless of how it does it. For now, you may fetch it from an external API. In the future, you may have a file, or even a database of your own AnimalSpecies, which you would like to use. You should be careful when abstracting the retrieval of AnimalSpecies, not to make an overly restrictive interface, nor an inefficient one. As I previously mentioned, having a Collection<AnimalSpecies> getAll() method may be inefficient. Maybe a Collection<AnimalSpecies> getSubset(Range) would make more sense. Then again, an external API may not conform to such an interface, so you may end up needing two repository interfaces, one for getting everything in bulk, another one to work with subsets.

The other service would then be responsible for creating an animal from the supplied AnimalSpecies.

Bringing it all together:

public interface SubsettingAnimalRepository {
  Collection<AnimalSpecies> getSubset(Range range);
}

public interface BulkAnimalRepository {
  Collection<AnimalSpecies> getAll();
}

public interface AnimalSpeciesSampler {
  AnimalSpecies getRandom();
}

public interface AnimalFactory {
  Animal createAnimal(AnimalSpecies species);
}

public classs AnimalService {
  Animal generateRandomAnimal() {
    return animalFactory.createAnimal(animalSpeciesSampler.getRandom());
  }
}

Notice that your AnimalService would then not care about the type of repository you end up picking in your specific infrastructure implementation, nor whether it allows a subset retrieval, bulk retrieval, or even direct sampling. You would be free to add whatever implementation you want, since the application layer only depends on a Sampler service to return it an instance of AnimalSpecies. You may mix multiple repositories within an instance, combine their results, and sample it that way. Neither the interfaces, nor the application layer itself would be aware.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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