Hexagonal Architecture - Introduction

An over­view of the con­cept of Hexag­o­nal ar­chi­tec­ture, al­so known as ports an adapter­s. It’s a well-­known good prac­tice of soft­ware en­gi­neer­ing to keep the busi­ness log­ic sep­a­rat­ed from ex­ter­nal, un­con­trolled de­pen­den­cies (im­ple­men­ta­tion de­tail­s), yet some­thing that keeps on hap­pen­ing re­peat­ed­ly. A de­sign with an hexag­o­nal ar­chi­tec­ture aims to solve this prob­lem, help­ing to achieve a clean ar­chi­tec­ture.

The main idea

The main idea be­hind the hexag­o­nal ar­chi­tec­ture is to de­sign our soft­ware in a way that ex­ter­nal de­pen­den­cies, or im­ple­men­ta­tion de­tails don’t pol­lute the main pur­pose of the soft­ware we’re build­ing: the busi­ness log­ic. Put in a dif­fer­ent way: an in­for­ma­tion sys­tem is a so­lu­tion to a par­tic­u­lar prob­lem. We build soft­ware be­cause we want some­thing done. In that pro­cess, of course we’ll have to deal with ac­ci­den­tal com­plex­i­ty (the tech­ni­cal nu­ances of the tech­nol­o­gy in­volved in what we’re build­ing), af­ter al­l, soft­ware does­n’t run in the vac­u­um. But we don’t build a soft­ware so­lu­tion just to tin­ker with tech­nol­o­gy, we do it be­cause we want some­thing done. And those tech­ni­cal nu­ances do tend to change over time. This hin­ders the main­tain­abil­i­ty of our soft­ware, mak­ing it more frag­ile over time. That’s why both as­pects need to be as iso­lat­ed as pos­si­ble.

For ex­am­ple, if we’re build­ing a ser­vice to present da­ta to cus­tomer­s, we can choose among sev­er­al tech­nolo­gies like gR­PC, HTTP (a REST API), GraphQL, and more. But none of those, whilst nec­es­sary for prac­ti­cal rea­son­s, should mat­ter to our busi­ness log­ic. Hence, in­ter­act­ing with ex­ter­nal ob­jects like HttpServle­tRe­quest, or an ORM en­ti­ty in the do­main lay­er is a bad prac­tice that ac­crues tech­ni­cal debt.

In­stead, ports and adapters ought to be used. Our do­main lay­er will in­ter­act with ports, and these ports will in turn, use adapters. That way, changes in ex­ter­nal de­pen­den­cies don’t af­fect the busi­ness log­ic at al­l.

What would be an ex­am­ple of a bad de­sign? Imag­ine a web ap­pli­ca­tion. In the do­main lay­er, one of the ob­jects needs to do some pro­cess­ing, so it takes as one of its pa­ram­e­ters the raw HTTP ob­ject pro­vid­ed by the web frame­work, then gets some pa­ram­e­ter­s, then writes a SQL query, fetch­es the da­ta and re­turns a re­sponse.

And how could we make that bet­ter? The same ap­pli­ca­tion, now in the do­main log­ic has an ob­ject that re­ceives an­oth­er ob­ject con­trolled by us (this is im­por­tan­t, it should be one of our port­s, not some­thing com­ing from an ex­ter­nal li­brary, frame­work, nor any­thing that en­tails tech­ni­cal de­tail­s). This ob­ject has spe­cif­ic meth­od­s, tai­lored to the needs of the ap­pli­ca­tion to re­trieve on­ly that ab­strac­tions and en­ti­ties that are need­ed, and then in­ter­acts with an­oth­er port that will fetch the re­quired da­ta from the repos­i­to­ry, and re­turn a spe­cif­ic en­ti­ty as need­ed. No HTTP de­tails in the busi­ness log­ic, no SQL ei­ther.

Some concepts

  • Do­­main log­ic: al­­so called busi­­ness log­ic is where the main en­ti­ties are lo­­cat­ed. This is where the pur­­pose of the soft­­ware is writ­ten, ir­re­spec­­tive of any oth­­er tech­ni­­cal de­­tail­s. The busi­­ness rules live here iso­lat­ed from any oth­­er ex­ter­­nal com­po­­nents.

  • Ports: are ways for the do­­main log­ic lay­er to com­­mu­ni­­cate with the ex­ter­­nal world. For ex­am­­ple, in or­der to re­­ceive da­­ta from a pri­­ma­ry ac­­tor (a client that’s us­ing the ap­­pli­­ca­­tion), this da­­ta will pass through a port, be­­cause the in­­te­­gra­­tion is­n’t done di­rec­t­­ly. In the same fash­ion, in or­der to in­­ter­act with a sec­ondary ac­­tor (like a re­pos­i­­to­ry, a place where da­­ta is stored), an­oth­er port will be used.

  • Adapters: An adapter will con­nect the in­­ter­­face of a port with one spe­­cif­ic im­­ple­­men­­ta­­tion of an­oth­er in­­ter­­face. This usu­al­­ly fol­lows the idea be­hind the adapter de­sign pat­tern.

For ex­am­ple if our web ap­pli­ca­tion needs to in­ter­act with a Dy­namoDB table, our do­main log­ic will have its own lay­er, with no im­ple­men­ta­tion de­tails re­vealed, and will in­ter­act with some­thing called Stor­age­Port for ex­am­ple, which will have a sim­ple in­ter­face to fetch and up­date da­ta. But this port won’t have any of Dy­namoD­B’s im­ple­men­ta­tion de­tails ei­ther. In be­tween, there will be an adapter, to pre­cise­ly adapt the in­ter­face from the spe­cif­ic driv­er or client need­ed for Dy­namoDB in­to the in­ter­face de­clared by Stor­age­Port.

A small example

Here’s a sim­ple class di­a­gram with a class/in­ter­face de­fined for each of the main three com­po­nents of the ar­chi­tec­ture.

https://www.plantuml.com/plantuml/png/ZL2zJiCm4DxlAMxaHnbO8uPIkY1r0Fe4XtD9B3c-O9z14VJTIK9Q2OUXa-MxxxwVRnELWgREOSiDDUB90LYl76eoZ3jIUgF83nNrumo_017nGso5gQz8-J55bOx31Bmoo-UfkeOZG7vy_rqKk1iyTRBRBiF_GSyIjGbyUDcVOB0QONcH3o_A66pJAagz9YxBzVsSyT2piRKrE6BnFN4OK0NdbP6kTmD-McrHMyPpNS2-maaGm3YASQxlbNk9LYKCItjvOlfzvztj9Pbc2NHSwsJk3eudkMsArdECUsciMTJ2MRxCx4ntcS6ReiZjmL_I4P7JRCRKgNC_

Note how we de­fine a port (in this case the DataRepos­i­to­ry with the in­ter­face we need for our ap­pli­ca­tion, and make the busi­ness log­ic ob­jects to in­ter­act sole­ly with this one. If at some point we need a Post­greSQL database, we cre­ate an adapter class that com­plies with the DatabaseAdapter in­ter­face by im­ple­ment­ing the re­quired method. All de­tails about con­fig­ur­ing the driver, and writ­ing the SQL queries are ab­stract­ed in this adapter class. If lat­er on we de­cide we need a dif­fer­ent database, an­oth­er adapter can be cre­at­ed to this end.

This is not on­ly use­ful for long-term main­tain­abil­i­ty of the pro­jec­t, but al­so as a tac­tic to speed de­vel­op­ment up. Struc­tur­ing the code this way, you could start your ap­pli­ca­tion with a very sim­ple stor­age so­lu­tion (a text file, or SQLite). If lat­er on, you re­al­ly need a pow­er­ful database, then you can de­fer that de­ci­sion, in­vest the cost of set­ting up that piece of in­fras­truc­ture, and then eas­i­ly plug it in­to the code by just writ­ing a dif­fer­ent adapter ob­jec­t.

Where to learn more

The orig­i­nal blog post 1 by Al­is­tair Cock­burn is a great source of in­for­ma­tion.

In ad­di­tion, this is a con­cept I men­tion in the lat­est edi­tion of my book (Clean Cdo­de in Python), in the chap­ter about clean ar­chi­tec­ture.

1

http­s://al­is­tair.­cock­burn.us/hexag­o­nal-ar­chi­tec­ture/