0

I would think the answer to my question is yes, but I have never really seen it in practice or any OSS project (and links to any OSS sample projects are highly welcome because the typical examples lack a lot of domain logic for me).

So, my specific case:

  • My application is mostly network-driven, that is, the UI is just a web API
  • If a request comes in, some logic is applied that can be described on an abstract level that a domain expert should understand
  • To actually do all the steps involved by the logic, external services are used (an external account management service)

My thinking to design this was:

  • Put the main logic into the domain layer by just calling interface methods
  • The interface is implemented on the application layer
  • My domain layer is therefore completely isolated and the application layer depends on it by implementing the service
  • In addition, the request handler (from the server, also application layer) will call my domain logic

Okay, so this is my domain pseudo code (Golang):

package domain

// all dates expect format Y-m-d (2022-12-01)

type HelperFunctions interface {
    getEMailConvertedToAccountName(string) string
    isDateInPast(string) (bool, error)
}

type AccountService interface {
    doesAccountExist(accountName string) bool
    createNewAccountObjects(accountName string, validUntil string) error
    updateAccountObjects(accountName string, validUntil string) error
    removeRelatedAccountObjects(accountName string) error
    retrieveUserIdentifier(accountName string) (string, error)
}

type UpsertUserJWTData struct {
    EMail      string `json:"email"`
    ValidUntil string `json:"valid_until"`
}

func UpsertUserAndGetIdentifierIfValid(data UpsertUserJWTData, helperFunctions HelperFunctions, as *AccountService) (string, error) {
    accountName := helperFunctions.getEMailConvertedToAccountName(data.EMail)

    dateInPast, err := helperFunctions.isDateInPast(data.ValidUntil)

    if err != nil {
        return "", err
    }

    if dateInPast {
        err := handleExpiredAccount(accountName, as)
        return "", err
    } else {
        if err := handleValidAccount(accountName, data.ValidUntil, as); err != nil {
            return "", err
        }

        identifier, err := as.retrieveUserIdentifier(accountName)
        if err != nil {
            return "", err
        }

        return identifier, nil
    }
}

func handleExpiredAccount(accountName string, as *AccountService) error {
    if as.doesAccountExist(accountName) {
        err := as.removeRelatedAccountObjects(accountName)
        return err
    }

    return nil
}

func handleValidAccount(accountName string, validUntil string, as *AccountService) error {
    if as.doesAccountExist(accountName) {
        return as.createNewAccountObjects(accountName, validUntil)
    } else {
        return as.updateAccountObjects(accountName, validUntil)
    }
}

What I like about this is that I directly see the logic itself. Logging, access to the real external service, helper function calls to get environment variables or config stuff are all not present here. I could discuss the sequence of events with a domain expert that does not know about any details of the implementation.

I could also create an entity for the account itself, true. But it has no real relationships to other domain objects yet (might change it later). My questions here are:

  • Do you think this makes sense?
  • If yes, how is this called in DDD-language (or maybe in clean architecture, hexagonal, I could not find any fitting term)? I mean, it resembles a bit the template method pattern and it does not just operate on domain objects but rather it defines "what happens if" in abstract/domain terms but the actual doings are abstracted away... maybe it's rather a "use case service"? This term does not exist though, I think
  • If this makes sense, why is it nearly impossible to find any examples where actual logic is implemented on domain layer in GitHub repos?
  • Are needed helpers like "dateInPast" nicely moved away here? It requires a second interface to be added here, but it really reflects the logic

PS: Yeah, code is suboptimal, some details like JWT could be excluded, maybe also how the username is retrieved. Some crunching would improve it more... but I hope the scope of my question is clear.

3
  • 1
    "To actually do all the steps involved by the logic, external services are used" - this is perfectly normal. Domain objects/functions/svcs don't necessarily do the actual steps, they (1) model (represent, encode) the domain elements and express the domain logic in their abstract behavior (== what client code that calls them sees and can know/infer about them), and (2) they orchestrate the internals to make that abstract behavior happen. They don't necessarily have to work as standalone things, without stuff plugged into them. The actual steps are an implementation detail. Jun 10 at 10:26
  • 1
    P.S. Some people here downvote way too easily and way too quickly, so ignore the initial couple of downvotes, unless it really gets out of hand. Letting the question stay longer increases its chances to get a good answer. I think its a good question and a good fit, as it actually asks about design. To members of the site: if you're thinking about downvoting, please leave a comment on how the question can be improved. Jun 10 at 10:37
  • After more reading and re-reading some books, I think, my approach is kind of fine, but it is probably just the "use case" layer from clean architecture that I use for this kind of logic. But with a classical DDD, I am not so sure where to put it. It certainly is a workflow and could as such live in application or domain layer... Maybe, this is just an example where perfectly sticking to the ideas is not the most helpful approach. I think, I'm okay with my structure... still would be curious for answers
    – IceFire
    Jun 10 at 17:41

0

Your Answer

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

Browse other questions tagged or ask your own question.