Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

Latest commit

 

History

History

pkg

Application Packages (pkg)

pkg contains everything necessary to serve the bork application. Its entry-point is defined in server.

This doc defines its various subdirectories. Note that directories prefixed with app don't have any special meaning other than providing uniqueness from standard library or popular package naming, and thus easier imports from the caller.

apis hold API definitions for the application. This layer sets up and fulfills the contract with calling clients.

The responsibility of this layer is to:

  1. Accept a request and marshal it into Go domain model equivalents.
  2. Call a service.
  3. Write a response.

Note that APIs can be defined in different architectures/protocols (REST, grpc, etc.), but should be composable without affecting the business logic of the application. The idea behind this is that APIs can evolve and update to modern standards without requires heavy changes to the rest of the code. Current definitions include:

appconfig is mostly a set of constants for referencing variables in a configuration implementation, and providing consistent access. It also contains some functional help for working with environments.

Note that it doesn't reference non-standard libraries and is agnostic to configuration implementation, like viper.

appcontext holds application specific getters and setters for Go's context. Context is one of the few layers shareable across layers of the codebase. It's most commonly used for injecting request/response information from the calling client. A common example is a User model for accessing role/authorization information.

As a general rule, context.Context should be passed down through the layers of your program, as this is the conventional Go way to address "cross cutting concerns", e.g. cancellation, logging, distributed tracing, or other types of instrumentation (which in other languages might be addressed via Thread Locals or similar constructs).

However, for any given function, concrete 1st order dependencies for the operation of that function should be called out as an explicit parameter, rather than buried in a call to the not-type-safe context.Value.

apperrors is a set of errors that adhere to Go's built-in error type. These represent a set of known errors that can occur in the application and are often one of two return values, coinciding with domain models.

apperrors provide a way for calling functions to act on an error return. Using basic errors (ex. errors.New()) either forces the caller to fail (ex. throw a 500 status code) or attempt to recover based on type-unsafe string comparisons. Instead, with typed errors, the caller can assert against the type and make decisions. An example in this code base is determining HTTP status codes based on service layer errors in the handler base.

Integration tests provide a way to test that the rest of the layers of code are set up properly. Since application layers are composable and independently testable, we need a way to test that the current application configuration runs.

Integration tests should mock out as little as possible, and run against a fully equipped server. In this codebase, the integration test setup shows an example running a test http.Handler based server.

Integration tests are slow and complex, thus should be minimal-- attempting to only test main code routes and integration availability. Wide coverage tests, should be left to layer based unit tests.

models contain types for passing data across multiple layers of the codebase. They are one of two returns values (coinciding with errors). They are used in different ways (ex. marshaling to JSON, retrieving from databases, performing business functions against), but do not provide function within themselves. The simple provide structured data for manipulation by other layers.

The goal of this layer, is to restrict functional requirements being shared across multiple layers of code. This forces simple contracts across layers, and in turn decouples functional implementation from data. APIs and data source become much easier to maintain and upgrade independently of each other.

server is the entry-point for serving the application. Its focus is to pull in configurations, set implementations from APIs, services, and data sources and execute their setup functions (ex. serve a route). server is the only place where configurations and implementations are directly referenced.

server also contains some other application orchestration, such as adding auth middleware and a logger.

Note that server should attempt to "fail fast" when it doesn't have the requirements to run. For example, if the application can't set up a database with configurations, check them and exit. Try not to let the lower levels of the codebase fail on knowable startup issues.

services contains the business logic of the codebase. The goal is that this layer can be run independent of APIs and data sources. It's purpose is to perform an action for a user. This is where user value is derived in the server application. This is opposed to other layers of the codebase, which tend to perform or adhere to technical constraints.

For example, API code may answer "how do I adhere to the dog REST interface?", a data source may say "how do I retrieve these dogs from postgres?", but the service answers the question of "how do I get the dogs that user 'x' owns?".

services defines a purely Go typed contract for parameters and returns, leaving data retrieval and marshaling to other layers. This allows it to be independently assessed, without knowledge outside of Go's compile time context.

sources provides packages to retrieve/set data to/from domain models. The most common example, is a adapter for a relational database, such as Postgres.

The goal of this layer, is to separate source implementation, such as client API code or query language from the rest of the codebase. The server package will then set up a source, based on application configuration. Within the context of a single request, the service layer will call functions on the source, but is agnostic to its implementation.