I am working on a project that is structured in hexagonal architecture. It is a multi module gradle project where web
layer is a separate module that depends on the domain
module.
Sample code related to the question:
class InvoiceController(val invoiceService: InvoiceService) {
@RequestMapping(..)
fun getInvoice(contractId: Int): Invoice {
return invoiceService.getInvoice(contractId)
}
}
class InvoiceService(val contractService: ContractService) {
fun getInvoice(contractId: Int): Invoice {
val contract: Contract = contractService.getContract(contractId)
//return invoice based on contract
}
}
Usually, the guideline is to put all business logic in service/domain layer, and keep controller layer just for handling request/response code. In above scenario, InvoiceService
depends on ContractService
just to get the required object.
Alternatively, we can have ContractService
dependency in the Controller class itself, and just pass the required Contract
object to the function, which makes the domain code much cleaner.
class InvoiceService() {
fun getInvoice(contract: Contract): Invoice {
//return invoice based on contract
}
}
But argument I hear from the team is that defining too many dependencies in Controller means we are putting domain logic in web layer.
Would like to know what would be the right/better approach.
2 Answers 2
The original DDD book talked in terms of a layered architecture; in particular a "user interface" layer, an "application" layer, and a "domain layer".
A controller would normally be considered part of the "user interface" layer: it happens that input data is delivered to you via the web, but it could just as easily be a desktop app or some sort of messaging queue.
Which is to say that there might be several difference slices between "the web layer" and "the domain".
class InvoiceService(val contractService: ContractService) {
fun getInvoice(contractId: Int): Invoice {
val contract: Contract = contractService.getContract(contractId)
//return invoice based on contract
}
}
One could reasonably split this implementation into a number of finely sliced methods
class InvoiceService(val contractService: ContractService) {
fun getInvoice(contractId: Int): Invoice {
val contract: Contract = contractService.getContract(contractId)
return getInvoice(contractId, contractService)
}
fun getInvoice(contractId: Int, contractService: ContractService) {
val contract: Contract = contractService.getContract(contractId)
return getInvoice(contract)
}
fun getInvoice(contract: Contract) {
//return invoice based on contract
}
}
The last of these is certainly "domain" code (such as you need domain for queries; see CQRS).
But the first two? To my eye, they look more like "plumbing" - in particular, if we need to worry about things like Contract Service throwing a network timeout, that would normally be a plumbing concern (application) rather than business concern (domain).
Another way of saying it: the application code is responsible for the orchestration between a Contract Service and a Domain Model.
Is the extra layer important, or just ceremony?
Design is what we do, when we want to get more of what we want than we'd get by "just doing it."
The extra layers stop being "just ceremony" when we find ourselves wanting to make changes within a layer (for example: replacing one web stack with another).
For parts that never change, the justifications for extra layers are mostly about readability and dependency management.
The machine itself doesn't care very much how you design the documents that describe its instructions.
I think the lower coupling, the better. I get you team's point, they're trying to hide the dependecy and keeping it centralized at InvoiceService internals. I also get your point, you're anyway coupling controllers and services by using a service on the controller, but they're looking for reducing that dependency to the bare minimum. Imagine that if you add both services into that controller, changes on the contractService would need changes at the invoice controller, which is far for being intuitive. You could consider it as a bad smell. Hope it helps!
-
3yeah the answer to the teams objection is just to add a new service layer which takes both the contact and invoice services as dependenciesEwan– Ewan2022年07月05日 13:22:50 +00:00Commented Jul 5, 2022 at 13:22
Explore related questions
See similar questions with these tags.