7 ms·
Master Hexagonal Architecture in Rust
- keyle 2y agoCould someone TLDR me what Hexagonal architecture means? Hexagonal architecture brings order to chaos and flexibility to fragile programs by making it easy to create modular applications where connections to the outside world always adhere to the most important API of all: your business domain. okay.
- Kostarrr 2y agoWell here's the take from the Author itself: https://alistair.cockburn.us/hexagonal-architecture/ https://alistair.cockburn.us/hexagonal-architecture/
- FridgeSeal 2y agoThat’s a mighty lot of words, to say “functional core, imperative shell”. Maybe I’m being glib, but damn, a whole article, and boatloads of fancy new terminology, just to re-state what the author succinctly landed on in the _first_ paragraph: > Create your application to work without either a UI or a database so you can <do a bunch of nice stuff and make your life easier>”.
- rapnie 2y agoIndeed. And some great HN discussions of a great article to get inspired on the pattern: - https://news.ycombinator.com/item?id=18043058 https://news.ycombinator.com/item?id=18043058 (Sep 2018, 127 comments) - https://news.ycombinator.com/item?id=34860164 https://news.ycombinator.com/item?id=34860164 (Feb 2023, 39 comments)
- FridgeSeal 2y agoI love a good rabbit hole, thank you!
- n42 2y agoDomain driven design with more hexagons and less Martin Fowler
- andyferris 2y agoAnother similar/related idea to Google is the “onion architecture”.
- andyferris 2y ago(Because the diagram gives a better TLDR than than the hexagonal equivalent diagram, IMO)
- keyle 2y agoyeah thanks I kept looking for a reason for the 6-way or 6-layers or something.
- mmcromp 2y agohttps://youtu.be/JubdZIdLQ4M?si=GkZ3tomeqIYzYyPk https://youtu.be/JubdZIdLQ4M?si=GkZ3tomeqIYzYyPk Best explanation by far, worth the 5min watch
- itronitron 2y agoThe bullshit remover condenses that down to the following: > Bullshit.
- goodpoint 2y agoPretty much nothing.
- sethammons 2y agoStep one: ignore the prefix, this has nothing to do with hex/six. And "ogon" meaning sides or struggle. The sides represent interfaces. This arch really means use interfaces based on business use cases / domains. Call the User service/module and pass user ids into a billing service/module. Each service is over a defined interface (adaptor) that allows separation of concerns and separate data stores. You could use an in-memory port of the billing service and a real db for the user service service because both implementations leverage the same adapter code
- regularfry 2y agoNo explicit dependencies in the core business domain, everything coordinates via interfaces defined by the needs of the core. That's pretty much it. Everything else flows from there.
- aswerty 2y agoI'm always surprised that this style of architecture isn't discussed in terms of functional purity [1]. To me, a hexagonal architecture essentially means creating an abstraction at the point between pure and impure functions. The realization of this approach essentially means the core of your application should be entirely pure and as large as possible. And your impure adapters, at the application boundary, (e.g. a rest api, db client, file system, system clock, etc.) should be as small as possible and impure. Doing this well essentially allows you to get the best of both worlds - highly coupled code (i.e.your pure functionality) and highly decoupled code (i.e. your pure-to-impure functionality). Another good reason for leveraging "functional design" as an argument is that many of those skeptical of architectural patterns are ironically heavily onboard the "functional design" bandwagon. So it is a strong argument in a political sense also. [1] https://en.wikipedia.org/wiki/Pure_function https://en.wikipedia.org/wiki/Pure_function
- keyle 2y agoYes after 25 years this is how I see it. Isolate state into its little dirty corner, think in terms of data structure and its transformation. This has kept me sane for decades. It's simple but so many devs just haven't seen the light yet.
- Kostarrr 2y agoCool article on how to abstract things in Rust. I must admit, I usually write the "bad rust application". Total nitpick: For `CreateAuthorError::Duplicate`, I would return a 409 Conflict, not a 422 Unprocessable Entity. When I see a 422 I think utf-8 encoding error or maybe some bad json, not a duplicate key in a database.
- slau 2y agoI agree that a duplicate key problem is 409. However I disagree that 422 is for encoding issues. Quite the contrary, 422 specifically says that “the server understood the content type of the request entity, and the syntax of the request entity was correct, but it was unable to process the contained instructions.”[1] So it’s more “your request didn’t make logical sense” more than “your request was missing a closing bracket”. That’s just a 400. [1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
- phamilton 2y agoHas anyone ever actually moved a mature application from one database to another and found that the code around calling the DB was a major painpoint? I'm all for the unit testing argument, but in an active SaaS business I've never seen the hypothetical database change where a well architected app makes it smooth. I have certainly moved databases before, but the performance and semantics changes dwarf the call sites that need updating. Especially in Rust, where refactoring is quite straightforward due to the type system.
- yuppiepuppie 2y agoMy anecdotal experience tells me that it never works in a high scale product environment. Having managed and lead 2 teams that maintained a legacy system with hex-arch and we had to move DBs in both. We ended up rewriting most of the application as it was not suitable for the new DB schema and requirements.
- phamilton 2y agoThanks for sharing. It matches my experience. After many years of a lean team serving high scale traffic (> 1 million monthly active users per engineer), most abstractions between customer and data seem to turn into performance liabilities. Any major changes to either client behavior or data model are very likely to require changes to the other side. There's a lot to be said for just embracing the DB and putting it front and center. A favorite system we built was basically just Client -> RPC -> SQL. One client screen == one sql query.
- mrkeen 2y agoI strongly believe in this principle, but I've also seen colleagues try to future-proof the database (via interfaces) in the wrong place. If your DBUserStore happens to know directly about SQL, that class is the wrong place to try to introduce flexibility around different DBs, ORMs, SQL dialects, etc. Just hard-code it to PostgresUserStore and be done with it. Instead, put the interface one level up. Your PostgresUserStore is just a place to store and retrieve users, so it can implement a more general UserStore interface. Then UserStore could just be a HashMap or something for the purposes of unit tests. Also, if you have some autowiring nonsense that's configured to assume "one service has one database", that's bad. Singletons used to be an anti-pattern and now they've been promoted to annotation and/or basic building block of Java microservices. When it comes time to live-migrate between databases, your service will need to stand up connections to both at once - two configurations, not one, so architect accordingly.
- dvt 2y agoThis is essentially one stop short of dependency injection[1] (even cited by the original "hexagonal architecture" author back in 2005[2]). I've been writing a lot of Rust this year, and even though part of me really likes it, I do miss Go's dumb simplicity. I have a feeling that you could spend entire sessions just architecting some fancifully-elegant pattern in Rust and not really getting any actual work done (just how I used to in Java). [1] https://www.martinfowler.com/articles/injection.html https://www.martinfowler.com/articles/injection.html [2] https://alistair.cockburn.us/hexagonal-architecture/ https://alistair.cockburn.us/hexagonal-architecture/
- mrkeen 2y agoI agree that this goes hand-in-hand with DI, but are you for or against it? E.g, Is Go simple because it allows switching out real DBs for hashmaps in unit tests, or because it forbids it?
- regularfry 2y agoYou absolutely can do the interface wrangling that Hexagonal wants in Go, if that's something you want to do. I've built apps that way. When I've gone sort of half-way and allowed explicit dependencies in the core (like on file I/O and so on) what I've regretted is those impurities, not the surrounding interface architecture.
- hi-v-rocknroll 2y agoThe sample code reminds me of PHP and Perl CGIs from 1999 where concerns are jumbled together. For a tiny hobby site that does one thing, sure, it's fine but for scalable, maintainable patterns there must be order, separation of looser concerns, and layering.
- SPBS 2y agoToo much layering. Code like this makes a huge song and dance around what is ultimately issuing an SQL query to the database and returning the results. The solution must always scale scale scale, never mind that simple problems should have simple solutions. A simple solution can always be refactored into a more complex solution, but not the other way round. It's always a safe bet to start with a simple solution.
- pistoleer 2y ago> If you ever change your HTTP server Never needed to. Premature optimization. > We have the same issue with the database What issue?? Cross that bridge when you get to it... > To change your database client – not even to change the kind of database, just the code that calls it – you'd have to rip out this hard dependency from every corner of your application. I really doubt this is such a big deal... Bit of ctrl+f for that module and paste the new one. All database libraries have a "connect()" and a "query()". I'm so far convinced that this article is written for people who have too much time to waste on things that will probably never happen just so they get to feel smart and good about something that's not even visible. Imagine if we built bridges this way: yes, right now we have settled on a stone masonry design, but what if we want to move to steel trusses in the future??? Why can software engineers not accept that it is unreasonable to expect one single human creation to last forever and be amendable to all of our future needs? Cross the bridge when you get to it. Don't try to solve future problems if you're not 100% certain you're going to have them. And if you _are_ certain, just do it right the first time, rather than leaving an opening for a future person to "easily" fix it up. And sometimes? A new bridge has to be built and the old one torn down.
- lnxg33k1 2y agoIt is much easier to change applications that are designed properly, regardless of the premature optimization, using adapters and interfaces is costless, and give you priceless peace of mind
- attheicearcade 2y agoI prefer the first example, to be honest. Much of the time your API is more or less a wrapper around the DB, so why introduce more indirection? I don’t really buy the testing argument since the interesting stuff which really needs testing is often in the queries anyway. Swapping out dependencies is not a huge issue in a language like rust, the compiler gives you a checklist of things to fix. I also don’t like that you call this function which handles transactions internally, inevitably you’ll end up with a handler calling two different functions like this resulting in two transactions when they should be atomic. At $work we’re slowly ripping out a similar system in favour of handlers calling database functions directly, such that they can put transactions in the right place across more complicated sets of queries. Code is simpler, data integrity is better.
- Kinrany 2y ago> Much of the time your API is more or less a wrapper around the DB, so why introduce more indirection? That's an easy scenario. General utilities should aim to make hard things possible and then minimize the overhead in easy scenarios, so the fact that this is more complex than the dumbest thing that could possibly work, is not by itself a good argument against.
- mrkeen 2y agoIt is for testing, but you don't indirect over class A because you want to test class A, you do so to test class B. By all means, write a test to make sure the queries actually work on the database. It will be slow, stateful and annoying, but still probably worth it. But you don't want to bring that slow-statefulness over to every other upstream test in the entire system: want to test if the permission system (in some outer class) is allowing/blocking correctly? You'll need to start up a database to do so. Is your test making sure that an admin with permissions can create a user (without error?), well it's going to start failing due to USER_ALREADY_EXISTS if you run the test twice. To avoid that you'll need to reset and configure its state for every single invocation.
- LargeWu 2y ago> To avoid that you'll need to reset and configure its state for every single invocation. Good testing frameworks just do this for you. I generally prefer focusing testing efforts around what you describe - spinning up the entire system under test - because that's how the system is used in real life. There's definitely times you want to test code in isolation statelessly, but I find that engineers often err on the side of that too much, and end up writing a bunch of isolated tests that can't actually tell you if an http call to their API endpoint performs the correct behavior, which is really what we want to know.
- FridgeSeal 2y agoHmmmmm. Mixed feelings about this. “Oh our http handler knows about the db” Ok? Its job here is more or less “be a conduit to the db with some extra logic”. It “knows about a lot of things” but it’s also exceedingly clear what’s going on. The main function is fine. I’ll take a “fat” main function over something that obscures what it’s actually doing behind a dozen layers of “abstraction”. Nothing like trying to isolate a component when you’re fighting an outage and nobody can figure out which of the 16 abstract-adapters or service-abstracted launched the misbehaving task. The original code might be “messy” but it’s at least _obvious_ what it’s doing, and it can pretty clearly be pulled apart into separate logic and IO components when we need to. This all just feels a bit…over engineered, to say nothing of the insulting tone towards the other learning resource.
- FridgeSeal 2y ago> Hard-coding your handler to manage SQL transactions will come back to bite you if you switch to Mongo I uuuhh, I hate to tell you this, but uh, if you’re swapping Postgres to Mongo, and you think that hardcoded queries are going to be your migration pain points, I have some bad news for you. The difference in semantics (for any such change, not just db) will bite you first, and wil bite a lot harder. This idea of “we can abstract over everything and then swap them out at our leisure” works less than we’d all like to imagine it does, and crucially building everything to accommodate this utopia, will leave you with mountains of code that will do nothing other than make your teammates hate you and obscure your actual logic. > AuthorRepository Oh hello C#. So instead of cursing everyone who has the misfortune of working on your C#-flavoured-rust codebase of having to write literal pages of boilerplate to inevitably add a new struct/type to the codebase, I suggest leaning into idiomatic Rust a bit more. Personally, I’d make read/delete/upset traits, whose methods take a handle to a connection pool. Logic for sql then lives inside these implementations, and can be brought into scope only when necessary. Something like `my_struct.upsert(&conn).await?`. We have locality of behaviour, we’ve separated IO details out, we have all the same advantages with about 50% less code-noise.
- neonsunset 2y agoDid you mean Java-flavoured? In C#, EF Core already implements a repository with the way its API is structured so most of the time writing another one on top of it is an anti-pattern.
- Kinrany 2y agoI wish Rust had a good dependency injection library. More specifically, a library that solves the problem of having long chains of constructors calling each other. The two main use cases are routing frameworks and tests. Axum already has this, but it is married to HTTP a bit too much.
- tcfhgj 2y agoWhat do you need a DI library for? What's the problem with constructor injection of the dependencies using traits?
- Fluorescence 2y agoI'm curious how mutability is meant to work e.g. in C# I might have a service that caches costly results in memory that I share across a number of other services operating in the same thread. Accessing something that might update it's internal cache needs to be mutable so i) this need for mutability is viral up the call chain ii) we can't share mutable references... so it's going to be a pain in the butt and need to sidestep compile guarantees somehow. Having an out of the box best solution for common scenarios like this would be nice to see at least.
- tcfhgj 2y agoThe solution is interior mutability - the concrete solution depends on the requirements (e.g. throughput)
- solidninja 2y agoThe problem is manual wiring (as always). It is fairly convenient to declare the source of your dependencies (somewhere around main) and have them be automatically wired in the sub-component graph, all without having to write out the chains of code to call constructor parameters. Also simplifies refactoring, as compile-time DI is mostly done on type and not on name or parameter position.
- Avi-D-coder 2y agoThis is such bad advice that I honestly couldn’t tell if it was a parody or not until I read the comment section—it’s not. Attempting these design patterns is a common part of getting over OOP when new to Rust. The result: over-abstracted, verbose, unmaintainable C++/Java written as Rust. Every layer of indirection ossifies the underlying concrete implementations. The abstractions inevitably leak, and project velocity declines. I have seen the same types and logic literally copied into three different repositories in the name of separation of concerns. Luckily people usually get over this phase of their Rust career after a couple of failures. If you’d like to skip that part, here are a few rules: 1. Always start with concrete types. Don’t abstract until you have at least two, preferably three, concrete implementations. 2. Separation of concerns is a myth. 3. K.I.S.S.
- tcfhgj 2y agoI don't want to invalidate your experience, but I would like to see what your claims and conclusions are based on.
- flohofwoe 2y agoNot the parent, but what's really missing in the article is a complete code listing of what the initial code has been turned into after the refactoring to really hammer home the absurdity of the advice (fwiw I was also scratching my head for a while whether this is satire, because the end result would look a lot like 'Java Hello World Enterprise Edition: https://gist.github.com/lolzballs/2152bc0f31ee0286b722 https://gist.github.com/lolzballs/2152bc0f31ee0286b722). The original code fits on one page, is readable from top to bottom, and doesn't contain any pointless abstractions that worry about 'future problems' that never actually come to pass in the real world anyway. If the code no longer fits the requirements, no big deal, just throw away those 40 lines and rewrite them from scratch to fit the new requirements. That will most likely take much less time than understanding and modifying the refactored 'clean code', because there's a pretty good chance that the new requirements don't fit the abstractions in the refactored version either (and IME that's the typical scenario, requirement changes are pretty much always unpredictable and don't fit into the original design, no matter how well thought out and 'flexible' the original design was).
- pantulis 2y agoI am not exactly fond of Hexagonal Architecture although I don't deny the merits of the idea and I think it's useful. That said the important thing is that the article was very well written and I've enjoyed reading it.
- atemerev 2y agoRust 2 Enterprise Edition
- resonious 2y agoIn my experience working in teams, it is _very hard_ to get people to adhere to these kinds of "clean code" separation of concerns style architectures. Even just nudging someone in the right direction and saying "hey we should have some kind of boundary here so that X doesn't know about Y" doesn't seem to result in any kind of long term shift. They'll say OK, adjust their code, and at that point you've already doubled the time it took them to work on the task. Then 6 months later, the same person will come in and break the boundary because they need to implement a feature that is hard to introduce without doing so. Even among people who read books on this stuff, it seems like very few of them are capable of actually carrying out the practice. And what's the point again? To make it so that you can switch out Postgres for MongoDB later? To make your web app now accessible over XMPP? It feels like a lot of work to enable changes that just don't happen very often. And if you wrote your app with Postgres, the data access patterns will not look very idiomatic in Mongo. I think X11 in *nix land is an interesting example of what I mean. X11's client-server architecture allows a window to be "powered" by a remote machine as well as local. But it's dog slow. Straight up streaming the entire desktop frame by frame over VNC is usually smoother than using X11 over the network. I think we just haven't reached a point yet where we can develop good apps without thinking about the database or the delivery mechanism. (I know X11 isn't exactly new, but even with decades of advancements in hardware, X11 over the internet still loses to VNC)
- deleted 2y ago[deleted]
- anacrolix 2y agoit's good in theory, and sometimes it pans out as your project evolves. however if you did this from the get go, you would never actually get anything done.
- John23832 2y agoThe example of a "bad" rust application is literally how any Axum application is written. I don't have the time to go look, but I'm pretty sure that the Axum examples are written as well. There's nothing wrong with it. If you want to test, test at the integration layer using testcontainers[0] or something. Not everything has to resemble a Spring style dependency injection/inversion of control. [0] https://github.com/testcontainers/testcontainers-rs https://github.com/testcontainers/testcontainers-rs
- seanhunter 2y agoI absolutely hate this "we'll get on to why hexagons in a moment" style that seems to be becoming more and more prevalent, where you make something seemingly the subject but you refuse to even define what it means for absolutely ages. Tell me the thing that's in the headline first. I'm not going to read your article if you don't do this. It's not that I'm not intellectually curious, it's that I don't like being messed around.
- 33a 2y agoThis seems really badly argued. The second version seems much worse and harder to extend. Looks like classic ORM style database abstraction wrapped with hand written types. This type of code usually leads to inflexible data models and inefficient n+1 query patterns. Relational algebra is inherently more flexible than OOP/ML-style type systems and its usually better to put as little clutter between your code and the db queries as possible in practice.
- SPascareli13 2y agoI remember a few years ago a very good engineer in our company shared a very similar article about hexagonal architecture in golang, and a lot of people started using it, including myself for some time. Now he's not here anymore, and posts in his linkedin about simplicity and how people overcomplicate things in software so much. This shows me that you should be careful when taking advice from other engineers, they learn and move on from what was previously a "best practice", while you might get stuck thinking it's worthy it because "that one very good engineer said it was how it should be done".
- belval 2y agoI might just be burned out but does anybody actually have time at work to do the "better" solution shown in the article? Never mind if it is actually better or not, but adding interfaces to abstract my DB package and actively decoupling everything seems like what was taught during my software engineering classes but rarely ever put in practice.
- vilunov 2y agoUnfortunately, my predecessors had plenty of time. Now I'm left with a burning project and half my time spent on clicking "go to implementation" of the next interface and hoping the first one will be the real one (actually full-text searching for it because Scala has no working IDEs).
- agubelu 2y agoHiding your logic and intent behind 10 layers of abstraction is also spaghetti code. Hell is full of single-implementation abstractions.