ESchouten

    ESchouten

    1 year ago
    Hi! I created a backend based on the Clean Architecture principles. Feedback would be very much appreciated! The application is separated into three modules: Domain, Usecases and Adapters • Domain module contains all entities, it's validation and repository interfaces • Usecases module performs actions on the domain entities and repositories and does authorization The domain and usecase modules do not have any external dependencies. • Adapter layer: each adapter is implemented as a standalone module, lowering dependence on specific frameworks and libraries and making them interchangable. The server module consumes all adapters (e.g. databases, (graphql) endpoints, authentication logic) GraphQL endpoints are auto-generated from the Usecases Used technologies: Ktor, JWT, Exposed, Flyway, KGraphQL/GraphQL generated endpoints.https://github.com/ESchouten/CleanArchitecture
    jmfayard

    jmfayard

    1 year ago
    Impressive work and I don't like it. Instead of a function
    fun changeOwnPassword()
    the supposedly clean architecture wants to force us to have a UseCase, a ChangeOwnPassword noun with an executor property that hides the verb that does all the work. What happened to functions as first class citizens? To me it seems like a step back to what Steve Yegge describes in his wonderful article "Execution in the kingdom of nouns" that describes very well why Kotlin is better than Javahttp://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html
    ESchouten

    ESchouten

    1 year ago
    @jmfayard Fair take Though while the implementation can always be changed, your main criticism seems to be on Uncle Bob's Clean Architecture principle itself, having Usecases instead of Service classes with functions. An interesting question I'm asking myself is: have functions been first class citizens since dependency injection? Instead of collecting somewhat coherent functions, or functions that work in the same domain, in a Service class, why not extract them to their own isolated service called Usecase? Add a little configuration and you've created your own public endpoint. I do however feel what you're saying, I, as well, tried mimicking functions by overriding the invoke operator function in Usecase, so the usecase can be called like
    changeOwnPassword(args)
    kqr

    kqr

    1 year ago
    do you consider usecases as application layer? why do you have repositories inside domain?
    ESchouten

    ESchouten

    1 year ago
    @kqr I do consider usecases application layer. Think 'service classes' with one function. Where in this case endpoints are automatically generated for. Very good question regarding the location of the repositories, I have been 'yoyo-ing' about that myself for a while. I came to this: A repository is a contract on how an Entity should be read, updated or deleted. While the Usecase layer is the only (probably) layer directly using these interfaces, it felt wrong separating them from the Entities. Furthermore, keeping Repositories in the Domain layer prevents developers from passing DTO's/models/other (function)interfaces as parameters and return types, keeping the Repositories purely for the domain entities, stimulating clean, maintainable code.
    kqr

    kqr

    1 year ago
    I don't understand last sentence, if repositories would be in application layer which depends on domain/entities anyway, what's the problem?
    ESchouten

    ESchouten

    1 year ago
    @kqr it isn't really a problem I guess, more a design choice/preference
    m

    Marc Knaup

    1 year ago
    Some screenshots from an actual project I’m about to launch 😄 I like keeping things simple and in functions. But at the end such classes keep popping up because there are things functions can’t do: • You cannot (easily) serialize function invocations. • Functions have limited overload ability while
    sealed class
    is more flexible and declarative on the input side. • Functions cannot contain metadata (needed for inspection/serialization/GraphQL/etc.). Nothing prevents you however from moving the logic out of the Executor into a plain function somewhere and just call it.
    The domain and usecases modules doesn’t have any external dependencies.
    I either misunderstand that one or disagree with it. What exactly is an “external dependency” here? For example common types from external dependencies are fine as part of the domain (e.g. date/time objects,
    Url
    object, etc.). You could wrap anything and everything but that has its downsides like plenty of boilerplate and performance & memory suffers. Also, using an external dependency can help reduce boilerplate code which distracts from the essential: the business logic.
    Domain module contains all entities, it’s validation and repository interfaces
    That one I try to avoid too. I separate the model (entities) from any logic whenever possible. The model is usually shared between server and client which is esp. handy with Kotlin Multiplatform. It can also be shared between multiple standalone parts of server. If everything is merged together then you cannot share the model independently of the underlying business logic (like validation, usually mostly server-side) and low-level access interfaces (repository interfaces). In larger projects it does make sense at some point to evolve and internal model independent from an external model. But that’s overkill in many projects.
    I’d improve the GraphQL example in the following ways:
    LoginUser
    doesn’t make sense on a
    query
    . It’s usually a state-changing mutation. • All fields should start lowercase. • All those root-level queries and mutations don’t need their own objects. It’s totally fine to have them all in the common roots
    Query
    and
    Mutation
    . • It’s “logInUser” (verb camel cased) not “loginUser” (noun) • I’d avoid confusing and meaningless names like
    a0
    . I call them
    input
    (like this: https://github.com/graphql/graphql-js/blob/main/benchmark/github-schema.graphql)
    Another thing that I’ve learned over time: Create groups/modules by business domain, not by their type (i.e. not 1 module for the entire model, 1 module for the entire database logic, 1 module for the entire API, etc.). E.g. for a blog I’d have a module “users”, a module “posts”, a module “comments” etc. That scales much better in back-ends as you can add and remove functionality module-wise since it logically belongs together. Some modules depend on each other, like comments on posts. And comments/posts on users. Here it gets a little tricky esp. if there are cycles involved. If those cannot be solved then each domain would need two modules. One that defines the domain and one that implements the domain.
    But there are exceptions to all of those. It depends on what I’m trying to build and how large/complex it is overall.
    ESchouten

    ESchouten

    1 year ago
    @Marc Knaup Thank you for your extensive review! • External dependencies in Domain and Usecase The main idea behind this is to make these core models and logic not dependent on a specific library, which may at one point change significantly/become deprecated etc. To me it isn't a hard rule, but something to aim for. In practice for me it means utilizing the Kotlin + Java stdlib and official/renowned libs e.g. (kotlin😆datetime. I tried preventing using wrapper types by using Kotlin value classes, but encountered some difficulities using reflection on them for the Graphql endpoints, will try to solve those soon. A good example on what library I would not like in the Domain or Usecase module is the PasswordEncoder/Validator. By creating an interface for that a lot of complexity is kept out of these modules.
    interface PasswordEncoder {
        fun encode(password: Password): PasswordHash
        fun matches(password: Password, hash: PasswordHash): Boolean
    }
    • Validation in Domain entities The only validation done at the moment in the domain Entities are field-level validation by the value classes, e.g. URL validness, password meeting requirements. Would that be a no-no for you? By sharing these models I guess these validations would port over. Fun fact: I started out with Kotlin-multiplatform for the Domain and Usecase module, but found it cumbersome on the Gradle level, thus left it out. When sharing the Domain module it would make sense to relocate the Repositories to the Usecase module, like @kqr suggested. Personally I never communicate the Domain Entities to other systems, always use different models/DTO's to prevent returning confidential data like User.password. • Naming conventions, Query/Mutation Thank you, noted them all down. Naming in general needs another look here, I'm not confident with several function and variable names. The a0 input variable (argument-0) is named this way to enable multiple input arguments in the future. Due to Java type ereasure I need to override the KType for the Graphql endpoint generation, and since I am doing this in a generic way the names need to be predictable. Obviously I could rename it to input-0 or even match the Ktypes ordinally with the arguments, which definitely would make it less confusing and meaningless 👍 • DDD Fully agree with grouping by Domain instead of type. The reason this is not (yet) done like this is the need of sorting into layers in order to enforce the modules access level to other dependencies. E.g. Usecases not knowing of the implementation of the PasswordEncoder, only the interface. By enforce this on a Gradle module level, instead of a developer-mindset level, we eliminate these errors all together. Ideally I would combine these two ways of grouping, but have no idea on how to do this in a nice manner, do you? Great, great review, many thanks! I'd love to hear more or hear feedback on the answers I've formulated above!
    m

    Marc Knaup

    1 year ago
    Thanks for the follow-up 🙂 I’d prefer to avoid reflection as much as possible. Compile-time info like
    KType
    is fine but anything beyond that should be no-go, esp. if it’s not multiplatform compatible. Most logic can be written without relying on reflection. Using value classes to hide external dependencies is okay-ish, but has its downside. You may end up wrapping another value class which at least adds a some performance hit. It again can add a lot of overhead that might not make it worth in smaller projects. I agree that it’s highly dependent on the dependency. Most foundational libraries like kotlinx-datetime should be fine.
    A good example on what library I would not like in the Domain or Usecase module is the PasswordEncoder/Validator.
    I agree. My concern was mainly about the model. Business logic may depend directly on external libs for smaller projects and incrementally abstract those away as the project grows. On a side note: Abstracting away the database for example is something you see quite often (think Hibernate & co). I really hate that approach. It adds incredible complexity and prevents the use of much database-specific functionality. Also, database libraries tend to be complex with big API surface so things like Hibernate are just re-inventing the wheel here. For me it’s usually a repository interface + an implementation that directly uses the database’s own library.
    The only validation done at the moment in the domain Entities are field-level validation by the value classes, e.g. URL validness, password meeting requirements.
    Those two examples are completely separate things for me. A valid URL is part of enforcing a correct datatype. Like having an
    Int
    value that’s not out of
    Int
    range. A valid password is high-level business logic that even may change over time. That shouldn’t be in the data model. I once had that with other fields and some kind of max-length. That quickly broke when rules changed over time and the database wasn’t able to load “old” objects that would no longer satisfy new rules. My
    Password
    value class is merely a wrapper around a
    String
    (can even be empty) with a
    toString()
    that obfuscates the actual password to not log it accidentally. What constitutes a valid password can be very complex logic, e.g. checking against special password tables etc. Not part of model. I don’t even validate for PW length on the client side. Client sends to server for validation. Single source of truth.
    prevent returning confidential data like User.password
    That would never happen in a good abstraction. a) You never save the password. You hash it. b) You should separate the user model from the identity model used by authentication. Identity/authentication is purely internal to the server and never part of the shared model.
    Due to Java type ereasure I need to override the KType for the Graphql endpoint generation, and since I am doing this in a generic way the names need to be predictable. Obviously I could rename it to input-0 or even match the Ktypes ordinally with the arguments, which definitely would make it less confusing and meaningless 👍
    That’s why I’ve started my own Kotlin-first multiplatform GraphQL library 🙂 https://github.com/fluidsonic/fluid-graphql (highly experimental, screenshot of example attached, although the code involves another library that does high level abstraction over my GraphQL lib as it doesn’t support that yet) I’ve also attached an example GraphQL schema generated by my libraries.
    By enforce this on a Gradle module level, instead of a developer-mindset level, we eliminate these errors all together.
    Ideally I would combine these two ways of grouping, but have no idea on how to do this in a nice manner, do you?
    I do separate by Gradle module and it was super helpful to find many code quality issues. I’m not sure what you mean by the other mindset. How would you separate it and where are the difficulties making it work with Gradle?
    ESchouten

    ESchouten

    1 year ago
    @Marc Knaup Good point on the Password field requirement validation 👍 Repository in this project is just an interface and the implementation also directly acesses the Exposed API User.password is a bad example for returning confidential data since it is not stored. I meant User.passwordHash. However, countless other examples could be made-up About the Gradle modules: What I meant was, when having a module per domain instead of layer, you can't control the layers accessing other layers which they should not. Maybe there could be Gradle modules per layer AND domain, but I am afraid to land in one big module configuration & folder hell. I'll definitely look into your Graphql library! Sounds very interesting!!
    m

    Marc Knaup

    1 year ago
    I meant User.passwordHash. However, countless other examples could be made-up
    Yup. Hence the separate model object for such data. In a larger project the authentication data is located in a completely separate back-end than the user object anyway (standalone auth server).
    What I meant was, when having a module per domain instead of layer, you can’t control the layers accessing other layers which they should not.
    Ah yes, you could mix update business logic and database logic for example. It’s difficult to separate that further without introducing more modules. Maybe different source sets would be an option? 🤔 There’s gotta be a compromise somewhere.
    My GraphQL high-level logic to define GraphQL types and operations needs a serious rework. It supports generics up to a certain point but that made the current code quite chaotic 😄
    The high-level logic is for now located in another project of mine: https://github.com/fluidsonic/raptor/tree/master/modules/graphql/sources-jvm
    ESchouten

    ESchouten

    1 year ago
    @Marc Knaup I am going to play around with Gradle modules, will update you if I find a somewhat workable solution KGraphql, the lib I am using atm, also needed a few PRs in order to properly support generics, I'll give yours a try! One of the pros of Clean Architecture is that you should be able to easily swap out libs, we could put it to the proof 😛
    m

    Marc Knaup

    1 year ago
    Mine isn’t really suitable for any end-users except me 😄 No documentation and quite interconnected with raptor (also no documentation)
    Both very experimental.
    ESchouten

    ESchouten

    1 year ago
    @Marc Knaup
    (standalone auth server)
    Which would be a good usecase for domain based modules