Hmm, can I have two :duct.server.http/jetty
servers on different ports with different handlers?
Tried out like this:
[:duct.server.http/jetty :dev/mock-server] {:port 2001
:handler #ig/ref :pot/mock-handler}
[:duct.server.http/jetty :http/main-server] {:port 2000
:handler #ig/ref :http/handler}
But getting an error about ambiquous keys: > Execution error (ExceptionInfo) at integrant.core/ambiguous-key-exception (core.cljc:71). > Ambiguous key: :duct.server.http/jetty. Found multiple candidates: [:duct.server.http/jetty :http/main-server], [:duct.server.http/jetty :dev/mock-server]
Ahaa! Turns out that I had a reference elsewhere to :duct.server.http/jetty
, which I forgot to turn into [:duct.server.http/jetty :http/main-server]
. Now it works ๐
I have some conceptual confusion I'd like some input on. Basically, I'm trying to figure out where domain logic should live. Boundaries should hide details like transactions and should be generally high level operations (like the transfer
example in the docs), but it also sounds like they should be only logic that's specific to that external service/db, so its not really the place for broader domain logic but rather just the aspects that relate to that external thing.
But sometimes domain logic is tightly intermingled (for example, what if there's some logic that has to happen within the transfer
operation/transaction but is really unrelated to the actual database logic of the transfer. How should this be handled? Higher order functions? Am I thinking about it wrong?
Any thoughts, tips or links on how to best architect things in duct greatly appreciated.
Thanks, although that's counter to what is said in the documentation, which is to prefer something like this:
(defprotocol AccountsDatabase
(transfer [db from to amount]))
over something like this:
(defprotocol AccountsDatabase
(with-tx [db callback])
(credit [db to amount])
(debit [db from amount]))
https://github.com/duct-framework/duct/wiki/BoundariesI actually started off with something like you describe, until I reread that
from that page: > Any functionality specific to the service should ideally be placed behind the boundary. For example, many databases support transactions, but it's bad practice to expose this through the boundary.
I guess we both need more supporting argumentation from the bad practice claim.
https://softwarecampament.wordpress.com/portsadapters/#tc2 I think its inspired from this
but not sure ๐
thanks! I'll give that a read (I'm aware of the hexagon architecture, but perhaps its time for a refresher)
one argument for hiding details like transactions is that different boundaries can be for different databases which require different transnational primitives (and can't be used together transnationally), so by not exposing transactions, you remove a potential error where you try to use these together in a single transaction when its not valid to do so
(and it hides the implementation details of talking to the external service/database)
but... that doesn't solve the domain logic issue, hence this question
I actually had a similar issue with boundaries and with-tx/with-conn. Had to send a bunch of redis commands within the same connection in a pipeline , im using a with-conn
macro-wrapper
, what I wanted was:
update-user-status
create-match
send-notification
but i ended up having to wrap this all in one protocol having a function called:
match-found
.
When I read โYou can have layersโฆโ in that article it hit home for me all of a sudden. The realization I had, which may still not be totally in agreement, is that if you write a boundary like I had mentioned above, you should also move it out of the application into a library. For us that could be a namespace/directory outside of the app namespace, or another project. And you would have another boundary (in the app) that would use that boundary (outside of the app). Layers of boundaries. Iโm not sure if that would be good practice and hexagon style? Seems like maybe?
so, boundaries become a part of the domain (the part that happens to be implemented by a database or external service) and the boundary can call lower level libraries
so transfer
is the domain and with-tx
, credit
and debit
are a lower level library that the boundary uses.
that does make sense
but i'm still unclear on what happens if domain logic needs to be intermingled. I can't think of a good example (perhaps that's a sign that its a non-issue in reality??), but imagine if during transfer you need to do something else. Currency conversion perhaps? Where does that logic go.
ok ok currency conversion can probably happen before transfer, but it seems rather low level if you have to get the currency from account A, currency from account B (meaning a boundary with low level getters is needed), calculate the values for both and finally pass those to transfer
but maybe that is the correct way to do it and the entire operation gets wrapped in a domain service:
service "transfer" gets data and does calculations
boundary "transfer" wraps the low level operations to call the library
library implements transaction, credit and debit functions.
But for fun, to throw another spanner in the works, what if currencies can change at any time and therefore conversion should be in the transaction?
I'm guessing if this is necessary, maybe the boundary functions should take a higher order function, right?
@teaforthecat I suppose if you can have low level boundaries and high level boundaries (high level are like the "good" boundaries in the documentation and low level like the "bad"), then it would work well enough. Then inject any additional domain logic by passing functions in, if necessary.
I know I'm rambling a bit, sorry ๐ thinking in text, I guess.
:duckie:
Nothing wrong with writing your thoughts down ๐
ok, it took me another read of that article, but I think Iโm getting it. Thanks so much for posting that @jarvinenemil!
You are welcome @teaforthecat โ๏ธ
That is an interesting question. Iโll share my thoughts, which are that the boundary protocol should shift away from the domain as much as possible. If the boundary is a db I think a good interface/protocol would be insert
,`update`,`delete`, or any more general methods that are not related to the domain, like execute
or something. In your scenario, you might need within-transaction
, which is very general and not domain specific. That is how I think about boundaries. Does that help at all?