Engineering
Top-down development with Golang interfaces

Golang interfaces encourage a top-down development approach. First, you write the top-level package. It can be

  • an HTTP handler if you develop a web application
  • a function that reads CLI flags if you are developing a CLI tool
  • an SDK function, if you are developing an SDK or just want to abstract your core logic from an entry point protocol like HTTP, CLI, gRPC, etc.
Second, you probably want to decompose your work while developing this package. For example, you want to have some abstraction to work with the persistence layer. At this point, you define the interface. You describe signatures of methods that you would like to have, for example:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>title</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <
type Storage interface {
	func Save(entity Entity) error
	func Read(id string) (Entity, error)
}>
  </body>
</html>
You don’t think so much about how exactly an entity will be persisted. Sometimes you even don’t have any idea about what database you are going to use. But you can still continue to develop your functionality. It is called the top-down approach, and it has its advantages and disadvantages. You can focus on the important functionality, not the details. You can mock your dependencies for development or test purposes.
Interface's implementation

When you finish your package, you go down the package tree and implement the interfaces you defined in the top-level package. These implementations must satisfy specific interfaces of the top-level package, which means they won't be very reusable. It happens because of two reasons.

  1. You follow the signature that was defined in the top-level package. However, other potential consumers define their own interfaces, and their signatures might not be the same.
  2. You import types (for example, 'Entity' in the example above) in your implementation to satisfy the interface. This type belongs to your top-level package.

It's important to note that Golang implements interfaces implicitly, and this is the explanation for this design on the tour.golang.org website:

"A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword. Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement."

This explanation is a little misleading because you must import types from the package that declares an interface anyway, for example, Entity. The only case when your implementation can really appear in any package without rearrangement, as claimed above, is when signatures of methods contain only primitive types, for example:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>title</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <
func Error() string>
  </body>
</html>
There is also a very popular decision to put your types not inside the package with the interface but in the separate third package, like this:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>title</title>
    <link rel="stylesheet" href="style.css">
    <script src="script.js"></script>
  </head>
  <body>
    <
webhandlers/ <- top-level package declares only interface, not types
webhandlers/webhandlers.go
storage/
storage/storage.go
types/
types/types.go  <- Entity is declared here.>
  </body>
</html>
But this approach didn’t remove the coupling between your implementation and interface at all. Because you still need to use the same types that are used in the package that declares interfaces.

It leads to the first problem of a top-down approach: strong coupling. Your implementation is developed with the caller in mind, which is ok in most situations. But, let’s be honest, when you are developing an application, you rarely want all your submodules to be reusable by other applications, not only your own. But suppose you realise that some piece of your code should really be independent. In that case, you’ll have to implement it without taking any interfaces into account to design a good API reusable by different applications.

Then you write some adapter package that satisfies the interface in the top-level package by relying on a new dependency that you developed. A common approach is to move such reusable packages into separate code repositories with different lifecycles to decouple them from your application.

The second problem of a top-down approach is that sometimes you cannot define interfaces without learning how underlying technologies work and what their API looks like. It is ok because you shouldn’t blindly follow any rules. Feel free to mix the top-down approach with the bottom-up approach. You start your application from the top-level module, but if you need to know the details or write them first, let’s do that.

Conclusion

Golang encourages you to write programs top-down with the interfaces feature. They are defined in upper-level packages (that need some dependencies) and implemented in lower-level packages (interface implementations). This way, our upper-level packages are decoupled from the dependencies, but our lower-level packages are still tightly coupled to their callers.

Sometimes it is ok, but if you think that some of your lower-level packages can be more reusable – forget about any interfaces and design this package with flexible and reusable API that different consumers can use. Then write adapter code/package that satisfies the interface in the upper package by adopting the lower package API. Mix top-down and bottom-up development approaches and write reliable software with Golang.

November 01, 2021

Vlad Tokarev
Backend Engineer