1. 4
  1.  

    1. 5

      I’m a little confused by the decision to pass the SQL transaction through the context? It seems like a way to sort of “cheat” the interface, which is fine I suppose but isn’t the entire point of the exercise to design a better interface? Also, the transactor and the store are necessarily tightly coupled (you can’t use a SQL transactor with a file store, for example) so it seems odd to separate them into distinct interfaces anyway–why not give the store explicit transaction semantics in the first place?

      Personally, I’m a little skeptical about the value of separating business logic from storage. I understand the appeal, but for things like databases, it’s often desirable to push business-logic concerns into the database layer, and attempts to subvert this result in code that is often inefficient–sometimes absurdly so–and you end up having to break the abstraction at various points anyway. I understand that the value proposition is that you can use one set of business logic rules for many different backends, but (1) how often do you actually need N different backends and (2) are the performance costs of supporting those backends (and the complexity costs of implementing a generic transaction solution) really less than the complexity cost of writing efficient business logic for each backend? Maybe someone can sell me on the premise of separating the business logic from the storage?

      Anyway, sorry if this seemed overly negative. For what it’s worth, I like the pass-a-function abstraction for things that have pre- and post-work. For example, I will occasionally write WithFile(func(*os.File) error) error functions so I don’t have to do the defer func() { if err := f.Close(); err != nil { ... } }() stuff all over.

      1. 1

        Maybe someone can sell me on the premise of separating the business logic from the storage

        I’ve used this kind of 3 layer pattern a lot over the years and I’ve settled on this more fuzzy/higher level idea of the third layer not strictly being “storage” but rather being a more general layer for data types, database access, readers/writers/persistence, invariants, etc. as a result, not everything in that layer for me is necessarily SQL/mongo stuff but also types and their respective methods (Account, Account roles logic, permission enums, message queue structs, etc)

        it’s been a handy structure over the years, and applying the pirates-of-the-carribean “they’re more guidelines than actual rules” to these 3-layer architectures like DDD/hexagonal feels like the way to go. I think these ideas are often designed in a vacuum but when you get to build real world codebases that evolve for years, you gotta bend the rules a bit and be a bit fuzzy - hence the more higher level general guidelines. This fits Go quite nicely I think, things like interfaces are flexible and simplicity is preferred over abstractions (I don’t write interfaces for DB repos for example, no point, because as you’ve pointed out, I’m never going to write implementations for 7 different databases)

      2. 2

        this makes spaghetti

        the ability to grab the db context from the go context.Context nullifies all abstractions

        1. 2

          The general idea is fine, but I see some limitations:

          • This wraps everything in a begin/stuff/commit block, even when all you want is a single select. Seems a bit of a waste.
          • Every PG query could result in a deadlock, so you need to be able to redo any query. This library is /almost/ there, since there’s already a callback which could be retried. It needs to test for the kind of error the callback returned, and call it again if it was a deadlock. But it does mean you can’t have any other side effects in the callback (like sending an email).
          • This doesn’t return any values. For example, the “increase balance” example could return the new balance. Sure, you could update a variable outside the callback (closure), but Go can now do generics, so something which can return (A, error) would be great.
          1. 1

            I’ve done this before, but not purely to get around the disconnected nature of DDD, but also to have transactions as a general concept - not everything may be an SQL transaction, you may want to perform some other kind of cleanup (delete a file from S3, cancel a context, revert some action, etc)

            quite a handy pattern! but the criticisms written here are all valid too.

            1. [Comment removed by author]