Layered software architectures adhere to the Dependency Rule: Source code in a lower-level layer can make use of code in higher-level layers, but never vice versa. Control flow, however, goes in both directions. How is this possible, given that higher-level code must not know anything about the code in lower levels?

Software Architecture

Software architecture strives to provide structure to software systems, in order to make them robust, maintainable, extendable, testable, easier to develop, and easier to document.

Many different architecture patterns have evolved over time, at different abstraction levels and with different levels of complexity. Among these architecture patterns, layered architectures seem to represent a fairly versatile concept that is applicable to a large range of scenarios.

Just to see how such an architecture may look like, let’s have a brief look at the Clean Architecture, a layered architecture model that summarizes the idea of layering very well.

The Clean Architecture

At the center of any layered software architecture is the separation of concerns. In simple words: The less each software module knows about the other modules, the better.

To achieve this, the modules are organized into layers. Each layer represents a certain level of abstraction. The Clean Architecture model describes them as (at least) four concentric circles, with the innermost circle representing the highest abstraction level.

(Discussing each layer in detail is outside the scope of this article. I briefly introduced the Clean Architecture here so that the dependency problem that is discussed below becomes clear. You can read more about The Clean Architecture in this article. Definitely recommended!)

The central rule of The Clean Architecture is the Dependency Rule, which says,

Source code dependencies can only point inwards.

In other words, the source code of each circle can only access code in an inner circle but never any code in an outer circle.

But what is this good for?

A small example without the Dependency Rule

As a completely made up and utterly pointless scenario, imagine a poet who writes, well, poems. Poems have to be stored somewhere, so the Chief Software Architect of ACME Poem Processing, Inc. comes up with this architecture:

A top layer (or “inner ring”) containing poem documents, and

A bottom layer (or “outer ring”) containing poem storage entities.

(Granted, this is a rather simplified version of a layered architecture but for our scenario, it is just enough.)

A document object obviously needs to access the services of a storage object to store and retrieve its contents (blue arrow). Thus it would seem natural to add a storage service directly to the document.

In our example, our poet surely wants to write the poems into a small notebook, and thus the Lead Programmer creates this document layer:

type Poem struct { content [] byte storage acmeStorageServices.PoemNotebook } func NewPoem () * Poem { return & Poem { storage: acmeStorageServices. NewPoemNotebook (), } } func (p * Poem) Load (title string ) { p.content = p.storage. Load (title) } func (p * Poem) Save (title string ) { storage. Save (title, p.content) }

Easy enough! But wait–what if our poet decides to write a poem on a napkin? Or on 4x6 index cards? The document layer would have to be modified and recompiled! We have created an unwanted dependency on a particular storage type.

How can we remove that dependency?

Abstraction to the rescue

As a first step, we can replace the storage service by an abstraction of that service. Using Go’s interface type, this becomes really easy.

type PoemStorage interface { Load ( string ) [] byte Save ( string , [] byte ) }

The interface describes only a behavior, and our Poem object can call the interface functions without worring about the object that implements this interface.

Now we can define the Poem struct without any dependency on the storage layer:

type Poem struct { content [] byte storage PoemStorage }

Remember, PoemStorage is just an interface but we can assign any type to storage that satisfies this interface.

Adding dependency injection

Right now the Poem only talks to an empty abstraction. As the next step, we need a way to connect a real storage object to the Poem.

In other words, we need to inject a dependency on a PoemStorage object into the Poem layer.

We can do this, for example, through a constructor:

func NewPoem (ps PoemStorage) * Poem { return & Poem{ storage: ps } }

When called, the constructor receives an actual PoemStorage object, yet the returned Poem still just talks to the abstract PoemStorage interface.

Finally, in main() or in some dedicated setup function, we can wire up all higher-level objects with their lower-level dependencies.

func main () { storage := NewNapkin () poem := NewPoem (storage) // wired up. }

Boom! We have just injected a dependency on a Napkin object into our new Poem object. To point it out again, at no point did the Poem object learn about the Napkin object, yet we just made it use one.

Please enable JavaScript to view the animation.

This is the gist of dependency injection. There is surely more to it than we were able to go through in this article. The interface/constructor pattern is not the only approach to implementing dependency injection. Still, it is a quite appealing one because it is clear and concise and builds upon just a few basic language constructs.

Verba docent exempla trahunt

Words teach, examples lead. With this in mind let me finish this article with a working example.

(Note: The complete lack of error handling or any other kind of sanity checks is intentional for brevity’s sake, yet it is anything but exemplary. If you think this sets a bad example for inexperienced readers, then you are probably right and I apologize. Dear inexperienced readers: Use proper error handling. Wherever you can. I am serious about this.)