5

I'm trying to get hands-on experience with Uncle Bob's Clean Architecture in Go, but I'm running into some issues. Also, I'm not yet familiar with all of Go's idioms.

For testing purposes, I'm applying the principles in a small accounting web tool with a REST API. Of course, I have modules like User, BankAccount, Transaction, and so on.

I'm structuring the layers as described in the book Clean Architecture with a Domain layer for my entities, an Application layer for use cases, an Infrastructure layer for implementations of interfaces like repositories, services, etc. and a Config layer for all the frameworks, databases and general configurations.

My main confusion is about where to define interfaces for repositories and services.

For example, I have an interface for UserScopeRepository that is used in most user-related use cases. The question is: should I define this repository interface inside each use case, or define it once in the user module/layer?

Also, I use UserScopeRepository in the RefreshToken use case. In that case, should I define the interface again in the use case layer?

The same goes for services like password hashing, JWT token generation, etc. Should I define the service interfaces inside each use case, or more generally in the application layer?

Or would it make more sense to put all services in a factory and pass them around?

geocodezip
3903 gold badges5 silver badges11 bronze badges
asked May 5 at 11:15

2 Answers 2

7

My main confusion is about where to define interfaces for repositories and services.

Generally speaking, in component design, there are two categories of interfaces: (1) provided interfaces (those that the component provides for its clients), and (2) required interfaces (those that the component requires of its plugins/collaborators).

When it comes to layering, you place interfaces (or abstract classes, or facade classes) that play the role (1)(provided interface) in the same layer, together with the implementation. The goal here is to expose a relatively narrow, well defined "surface area" for the clients to use, minimizing the amount of things they can be coupled to, and hiding the details of how it is all implemented behind the scenes (so that it's easier to vary them).

When an interface is in the role (2)(required interface), you still think of the component that requires it as being the "owner" of the interface (as opposed to the implementing component), and you define the methods on the interface in terms of the specific needs of the owning component, rather than in terms of the general capabilities of the class that's going to implement it. The required interface and the owning component form a sort of a cohesive unit that lives in the same layer. If this seems strange to you, recall that this same idea is behind the Strategy Pattern. The implementation of the interface then provides some specific service to the component, and may be defined in a different layer.

In terms of Clean Architecture, if the implementation is defined in an outer layer, then this is a form of dependency inversion - the dependency arrow (in this case, from the implementation to the interface) originates in an outer layer, and points towards an inner layer, and is conditioned on the needs of the component that owns the interface (because, remember, that's what guided the design of the interface), while the control flow goes in the opposite direction. The key idea here is to find an interface design that captures the needs of the owning component (see below) in a way that makes the interface more stable in the face of changes compared to both the owning component and an implementation. You would normally use dependency injection (constructor injection, most commonly) to inject some chosen implementation into the component at the composition root.

Interfaces to repositories and services are of this second kind, generally speaking. You design them with respect to the needs of the owning Use Case Interactor, whith methods that are a reflection of these needs, and then you implement those interfaces in an outer layer. I.e., instead of having an interface to a repository define general-purpose CRUD operations, you define methods that are a bit more high-level, e.g., something like GetAdminUsers(accessTier), and you do the CRUD-y stuff behind the scenes, in the implementation of the method. The tricky part is to come up with a stable set of methods that still allows you to express the logic that the use case needs to implement.1 The takeaway here is that merely placing an interface between two classes doesn't magically decouple them - you need to come up with the right set of methods (right abstraction).

There are circumstances where you might want to take a different route, though. E.g., if your domain involves users constructing arbitrary queries, or there is a lot of complexity in the kinds of queries you need to do - then you might instead prefer using some kind of (possibly domain specific) query language, and part of the domain logic would revolve around the rules governing this query language.

Clean architecture - details

The diagram above comes from Martin himself (and IIRC you'll find a similar graphic in the book). The Data Access Interface is essentially a gateway interface to what you call a repository - note that the interface is on the same side of the architectural boundary (the double line) as the use case, but its implementation isn't. Also, don't take this diagram as gospel - the exact elements shown aren't as important as the relationships between them. The various boxes shown are meant to represent architectural elements of the CA model, rather than be a literal blueprint, and your specific application might end up looking slightly different. For example, Input Data may be an explicit struct of some sort, or it may simply represent the parameter list on a method on the Input Boundary interface. Or, if there's a need for it, the Use Case Interactor may delegate part of the logic to one or two other classes not shown on the diagram.

Now, how exactly you distribute various responsibilities across different classes is up to you (and your team). You can have one repository (or service) implement several different interfaces, possibly coming from different use cases. You can have one use case require two different interfaces (two dependencies playing different roles), that are initially both implemented by the same class (this seems odd at first, but it allows you to replace one of the dependencies independently). You can have a distinct repository implementation per use case, where all of those repositories may or may not use the same underlying lower-level object/library internally. They may or may not all interact with the same database. Or you can mix and match to suit the needs of the project.

If you have a repo or a service that represents a concern that cross-cuts across use cases, it's fine to share the same implementation across all of them, assuming there is no compelling reason not to share, and assuming the implementation does not depend on the specifics of any particular use case. You can think of such an interface as conceptually being owned by the layer itself, or perhaps by some other logical grouping of components within the layer. Note also that "sharing an implementation" does not necessarily mean that the same instance of the object is given to all the use cases, though it could be (again, you'll have to decide what to do here given your problem domain, the constraints you face, etc).

As a side note, while CA puts a decent amount of emphasis on layering, it doesn't require you to structure the project so that it's physically divided into said layers (it's a common misconception). The layers are primarily a logical separation. You are also expected to separate concerns within layers, yielding granular components, and then decide how you want to package them up (by layer, across layers by feature (vertical slices), or some other way). IMO, it's disadvantageous to make this decision at the very start of the project (or let someone else make it for you by downloading a canned project template), and then get locked into it.


1 Note that you're not expected to get the design perfectly right from the very beginning. Come up with something based on your initial understanding of the domain, and try to make (and embed in code) as few assumptions as possible (follow the YAGNI & KISS principles). As you learn more about the domain, adjust the design from time to time, when you feel that there are aspects of the design that are starting to get in your way (choices you made that turned out less than optimal). And do it judiciously, always striving to restructure things so that the codebase becomes more comfortable to work with in light of your expanded understanding of the problem domain, as opposed to religiously following generalized "best practices" or overcommitting to earlier decisions for "consistency" - these lead to overengineering, spaghetti code, and you and your team spending more and more of your time trying to keep together the contraption that the application has become.

answered May 5 at 15:03
2
  • 1
    Thank you very much. This is a perfect answer to my question. That makes sense now. Commented May 6 at 12:18
  • @SamuelObisesan Thank you for taking the time to improve the answer. I've accepted most of your edits, but I elected to rephrase some parts of the answer for clarity. There was one edit that didn't reflect what I meant, but that's on me for phrasing it clumsily: "You declare them [-with respect to] [+to] the owning Use Case Interactor". I see two potential ways to misinterpret this: the interface is neither declared "to" the UCI, nor is it an "interface to" (implemented by) the UCI. What I meant to say is that the design of the interface should be driven by UCI's design considerations. Commented May 6 at 21:58
4

My main confusion is about where to define interfaces ...

enter image description here

When you're crossing an architectural layer boundary, CA is very picky about where the interface is defined. In that case, interfaces are defined in the inner layer. This prevents the inner layer from knowing any details of the outer layer which must conform to these interfaces. This makes the outer layers easy to replace and makes the expectations of the inner layer clear. It also prevents changes from spreading. That is, if you get the abstraction right.

This is a CA plugin. It's designed so regardless of how control flows, dependencies point inwards. That isolates knowledge while allowing control to go where it's needed. It's basically the DIP pattern folded over. By not using return to come back out of the lower level, instead you're again facing an abstraction as the flow moves through the output port. You're also not being forced to send the response back to the caller and can send it where it's needed. Sounds complicated but it's how you're meant to use objects. Alen Kay called it message passing.

This actually helps encapsulation because rather than being tempted to return your objects private innards you have an interface that defines how to talk to whatever output is going to.

... for repositories and services.

Those seem like Interface Adapters.

enter image description here

For example, I have an interface for UserScopeRepository that is used in most user-related use cases. The question is: should I define this repository interface inside each use case, or define it once in the user module/layer?

I wouldn't define a repository for every use case. I'd hope to abstract the concept of user persistence in some reusable way and use it to abstract the user part of the DB. If some user logic represents some business rule I'd try to keep that out of the repository.

enter image description here

I discourage you from seeing any of these lines as always being 1 to 1

Also, I use UserScopeRepository in the RefreshToken use case. In that case, should I define the interface again in the use case layer?

Multiple use cases are allowed to share the same interface. You don't need to define it twice.

The same goes for services like password hashing, JWT token generation, etc. Should I define the service interfaces inside each use case, or more generally in the application layer?

A use case and the use case layer are not the same thing. The interfaces CA speaks of live in the layers. Any interface that you cram inside a particular use case isn't something CA even has an opinion about.

Or would it make more sense to put all services in a factory and pass them around?

That would be a service locator. Which is not CA at all.

answered May 5 at 17:37
1
  • Thank you very much. This helps alot understanding the concepts. Commented May 6 at 12:24

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.