September 12th, 2024

Master Hexagonal Architecture in Rust

The guide on hexagonal architecture in Rust promotes scalable applications by emphasizing separation of concerns, introducing the repository pattern, and providing practical examples for effective implementation and incremental learning.

Read original articleLink Icon
Master Hexagonal Architecture in Rust

This guide on hexagonal architecture in Rust aims to help developers create scalable and maintainable applications. It begins by illustrating the pitfalls of a poorly structured Rust application, which often leads to hard dependencies and tightly coupled code that is difficult to test and refactor. The guide emphasizes the importance of separating concerns, advocating for a design where different components of the application interact through well-defined interfaces. By adopting hexagonal architecture, developers can encapsulate dependencies, making it easier to swap out components like databases or HTTP servers without extensive rewrites. The guide also introduces the repository pattern, which helps abstract data storage, allowing the application to remain agnostic of the underlying database technology. Throughout the tutorial, practical examples are provided, including a blogging engine built with the axum web server, demonstrating how to implement these architectural principles effectively. The author encourages readers to engage with the material incrementally, digesting each section before moving on to the next, ensuring a thorough understanding of hexagonal architecture's benefits and trade-offs in Rust development.

- Hexagonal architecture enhances maintainability and scalability in Rust applications.

- The guide emphasizes the separation of concerns to avoid tightly coupled code.

- The repository pattern is introduced to abstract data storage and improve flexibility.

- Practical examples, including a blogging engine, illustrate the implementation of these principles.

- The tutorial encourages incremental learning and application of concepts.

Link Icon 21 comments
By @Avi-D-coder - 7 months
This 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.

By @attheicearcade - 7 months
I 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.

By @phamilton - 7 months
Has 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.

By @FridgeSeal - 7 months
Hmmmmm.

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.

By @dvt - 7 months
This 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

[2] https://alistair.cockburn.us/hexagonal-architecture/

By @seanhunter - 7 months
I 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.

By @pistoleer - 7 months
> 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.

By @SPascareli13 - 7 months
I 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".

By @pantulis - 7 months
I 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.
By @John23832 - 7 months
The 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

By @33a - 7 months
This 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.
By @belval - 7 months
I 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.
By @agubelu - 7 months
Hiding your logic and intent behind 10 layers of abstraction is also spaghetti code.

Hell is full of single-implementation abstractions.

By @Kostarrr - 7 months
Cool 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.

By @Kinrany - 7 months
I 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.

By @keyle - 7 months
Could 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.
By @resonious - 7 months
In 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)

By @hi-v-rocknroll - 7 months
The 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.
By @atemerev - 7 months
Rust 2 Enterprise Edition
By @anacrolix - 7 months
it'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.