4

The book "Implementing Domain Driven Design" (page 361) suggests to use special types to distinguish several kinds of IDs, e. g. using BookId(1) instead of just 1 of type Int or Long. In my Clean Architecture or Onion Architecture the outermost layer is a HTTP adapter taking an id of type Long. Then this adapter calls an application service, which in turn calls the domain (a repository in this case). The flow is as follows:

HTTP Adapter → Application Service → Domain

The question is where to convert the Long to BookId. In principle there are two options:

1) Convert it in the HTTP adapter. This would have the benefit that the raw type would no longer exist and no one could ever use an AuthorId where a BookId should have been used. The conversation could even be performed by the framework, so that no code would ever have to convert or handle raw types. An example with Jersey (JAX/RS) could look like this:

@GET
@Path("/books/{id}")
fun findById(id: BookId): Response { ... }

A converter registered by the framework would take the Long value from the request and convert it to a BookId automatically.

HTTP Adapter  → Application Service → Domain
Long → BookId   BookID                BookId

2) The Long value could be passed to the application service. However, I don't see any benefit here.

HTTP Adapter → Application Service → Domain
Long           Long → BookId         BookID

Question: Where is the best place for the conversion? Do you see any good reason not to convert the value as early as possible?

  • where do you convert your complex types? – Ewan Feb 14 at 15:10
  • 1
    It's not really about converting primitive types, but rather about converting framework/library–specific types; this is obscured here by the fact that the type in question is long. The interface of the adapter is defined by the Application Service, and resides in the same layer; as such it is expressed in terms of types and abstractions decided by the Application Service. Imagine now that it's not long, but some library-specific data structure - then it becomes more obvious; if you do (2), then App Service has a dependency on HTTP Adapter (and the lib) - which breaks the dependency rule. – Filip Milovanović Feb 14 at 17:51
2

I recently implemented exactly this type of conversion in a framework for building HTTP services. I chose to implement it at the framework boundary, such that my project code only ever sees the BookId style types. One of the benefits (and the motivations for doing it this way) is that if I define my functions in terms of non-primtive types, I can automate certain types of tedious validations and automatically return meaningful errors. For example, my framework will automatically return an HTTP 400 or 422 for any request data that doesn't satisfy the invariants of the "meaningful type" in the signature.

Here's an example (the framework is for F#, but the concepts are universal):

[<HttpGet("[version]/invoices/{invoiceNumber}")>]
let getInvoice (invoiceNumber: InvoiceNumber) =
    api {
        let! repository = inject<IInvoiceRepository>()
        let! invoice = repository.GetInvoiceByInvoiceNumber(invoiceNumber)
        return
            match invoice with
            | Some i -> Http.ok(i)
            | None -> Http.notFound()
    } 

So let's say, in this example, that InvoiceNumber is one of our meaningful primitive types, and it's defined as a 10-digit non-negative integer. You would define some conversion functions that do the validation, and then the framework would automatically decompose the HTTP request and attempt to validate the request data using the conversion function. If it fails, the framework never even calls the function and automatically returns a validation error with the details of the failure. If it works, the framework invokes your function, and you never see the underlying primitive.

/// Must be a 10-digit numerical string
[<Struct; Validated>] type InvoiceNumber = private InvoiceNumber of string

module InvoiceNumber =
    let create = 
        validation<InvoiceNumber> {
            rule Rules.isNumerical InvoiceNumberMustBeNumerical
            rule (Rules.isLength 10) InvoiceNumberMustBeTenDigits
            rule Rules.isPositive InvoiceNumberMustBePositive
        }

    let value (InvoiceNumber invoice) = invoice

If you're curious about how all this is handled in the framework, you can take a look at the GitHub repo. I haven't completed the documentation yet, but the code is available.

2

A) There are places in your code base that do not know that bookId exists.

B) There are places in your code base that do know that bookId exists.

C) There are places in your code base that know that bookId is really a long.

Maximize A. Minimize B. Minimize C even more.

Not knowing, when you don’t need to know, is a good thing.

  • +1 for the succinctness – VoiceOfUnreason Feb 14 at 19:26
  • I'm curious whether you think a maximized A can ever overlap with "Places in your codebase that have to work with Books"? – Aaron M. Eshbach Feb 14 at 19:54
  • I understand A, B) with the demand to minimize them as some kind of modularisation or decoupling (avoidance of a tangled mess). C) Would support my favorite approach with conversion in the outermost layer. Did I understand this correctly? – deamon Feb 15 at 20:54
  • 1
    @deamon yes, in each case, in each way, seek to know as little as you can. But not less than that. Do not force things to know things unless you have to. Spread knowledge reluctantly.Hide details behind abstractions. long is a detail. It's a design decision. Make it in one place. Only break down and admit it's a long when talking to a system that refuses to learn your abstraction. Even then, look at what abstractions are available in the other system. Maybe you can do better. – candied_orange Feb 16 at 2:19
0

I doubt many people bother with this kind of 'lets avoid primitives' typing and you may have touched on one of the reasons why.

Converting in the outermost layer seems like the obvious choice. As you say the framework should be able to handle such things.

However, in practice its a bugger to debug when it doesn't work and you may find you need to setup special conversion classes if you want to convert primitives to anything other than their counter part in you language of choice.

If you framework handles it without complaint, great. If its fiddly I would do it in the controller pre service call. After all you want all your domain code to make use of the special type and not have to worry about converting primitives

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.