Code structure

Armed with the framework provided by getting to know our users, we are ready to think about implementation and code structure.

Given we are making a standalone service, we are going to need a main() function. After that, the next thing I always add is an internal folder directly under main(). This adds a clean boundary between the code for this service and any code in the same repository. 

When you are publishing a package or SDK for others to use, this is an easy way to ensure your internal implementation packages do not leak into the public API. If your team happens to use a mono-repo or multiple services in one repository, then it's a great way to ensure that you do not have package name collisions with other teams.

The layers we had in our original service were relatively normal, so can reuse them here. These layers are shown in the following diagram:

The main advantage of using this particular set of layers is that each layer represents a different aspect required when processing a request. The REST layer deals only with HTTP-related concerns; specifically, extracting data from the requests and rendering the responses. The Business Logic layer is where the logic from the business resides. It also tends to contain coordination logic related to calling the External Services and Data layer. The External Services and Data will handle interaction with external services and systems such as databases.

As you can see, each layer has an entirely separate responsibility and perspective. Any system-level changes, such as changing a database or changing from JSON to something else, can be handled entirely in one layer and should cause no changes to the other layers. The dependency contracts between the layers will be defined as interfaces, and this is how we will leverage not only DI, but testing with mocks and stubs.

As the service grows, our layers will likely consist of many small packages, rather than one large package per layer. These small packages will export their own public APIs so that other packages in the layer can use them. This does, however, deteriorate the encapsulation of the layer. Let's look at an example.

Let's assume that we have performance issues with our database and want to add a cache so that we can reduce the number of calls we make to it. It might look something like that shown in the following code:

// DAO is a data access object that provides an abstraction over our 
// database interactions.
type DAO struct {
cfg Config

db *sql.DB
cache *cache.Cache
}

// Load will attempt to load and return a person.
// It will return ErrNotFound when the requested person does not exist.
// Any other errors returned are caused by the underlying database or
// our connection to it.
func (d *DAO) Load(ctx context.Context, ID int) (*Person, error) {
// load from cache
out := d.loadFromCache(ID)
if out != nil {
return out, nil
}

// load from database
row := d.db.QueryRowContext(ctx, sqlLoadByID, ID)

// retrieve columns and populate the person object
out, err := populatePerson(row.Scan)
if err != nil {
if err == sql.ErrNoRows {
d.cfg.Logger().Warn("failed to load requested person '%d'. err: %s", ID, err)
return nil, ErrNotFound
}

d.cfg.Logger().Error("failed to convert query result. err: %s", err)
return nil, err
}

// save person into the cache
d.saveToCache(ID, out)

return out, nil
}

However, there is no need for the existence of this cache to be visible to the Business Logic layer. We can make sure that the encapsulation of data layer does not leak the cache package by adding another internal folder under the data folder.

This change might seem unnecessary, and for small projects, that's a good argument. But as the project grows, the little cost of adding an extra internal folder will pay off and ensure that our encapsulation never leaks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.0.145