Getting started with Go?

My book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

The ‘fat service’ pattern for Go web applications

Published on:

In this post I'd like to talk about one of my favorite architectural patterns for building web applications and APIs in Go. It's kind of a mix between the service object and fat model patterns — so I mentally refer to it as the 'fat service' pattern, but it might have a more formal name that I'm not aware of 🙃

It's certainly not a perfect pattern (we'll discuss some of the pros and cons later) — but it is (deliberately) simple, pragmatic, and I find it often works well for small-to-medium sized projects.

At a high-level, the fat service pattern splits your project code into two distinct 'layers':

  • The application layer. This contains your code related to reading and writing HTTP requests and responses, authenticating/authorizing requests, session management, etc.

  • The service layer. This contains your business logic, defines your core data types, and is also responsible for interacting with any persistent data stores.

A fat service example

Let's illustrate how this pattern works with an example of a JSON API.

Specifically, let's say that we want to build an API with a POST /register endpoint which is used to register a new user. When a client makes a request to this endpoint, let's pretend we want to take the following actions:

  1. Parse the JSON input into a Go struct so we can work with it easily.
  2. Carry out some validation checks on the data (and return an error response to the client if any of them fail).
  3. Create a hash of the new user's password.
  4. Insert a record for the user into a database.
  5. Send a notification to a Slack channel to say that a new user has registered.
  6. Return a 204 No Content response to the client if everything worked successfully.

Using the fat service pattern, we could structure our project so that the directory and file layout looks like this:

.
├── cmd
│   └── api
|       ├── handlers.go
│       └── main.go
└── internal
    └── service
        ├── service.go
        └── users.go

The cmd/api package will contain the application layer code, and the internal/service package will contain the service layer code.

Then, very roughly, the code in our service layer might look something like this:

File: internal/service/service.go
package service

import (
    "database/sql"
    "errors"
)

var ErrFailedValidation = errors.New("failed validation")

type Service struct {
    DB              *sql.DB
    SlackWebhookURL string
}
File: internal/service/users.go
package service

import (
    "github.com/slack-go/slack"
    "golang.org/x/crypto/bcrypt"
)

type RegisterUserInput struct {
    Username         string            `json:"username"`
    Password         string            `json:"password"`
    ValidationErrors map[string]string `json:"-"`
}

func (s *Service) RegisterUser(input *RegisterUserInput) error {
    input.ValidationErrors = make(map[string]string)

    if input.Username == "" {
        input.ValidationErrors["username"] = "must be provided"
    }

    // And any other validation checks...

    if len(input.ValidationErrors) > 0 {
        return ErrFailedValidation
    }

    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), 12)
    if err != nil {
        return err
    }

    _, err = s.DB.Exec("INSERT INTO (username, hashed_password) VALUES ($1, $2)", input.Username, string(hashedPassword))
    if err != nil {
        return err
    }

    msg := slack.WebhookMessage{
        Username: "robot",
        Channel:  "#general",
        Text:     "A new user has signed up!",
    }

    return slack.PostWebhook(s.SlackWebhookURL, &msg)
}

And the code in our application layer might look like this (I've omitted the helper functions for brevity):

File: cmd/api/main.go
package main

import (
    "database/sql"
    "flag"
    "log"
    "net/http"
    "os"

    "example.com/internal/service"

    "github.com/alexedwards/flow"
    _ "github.com/mattn/go-sqlite3"
)

type application struct {
    logger  *log.Logger
    service *service.Service
}

func main() {
    dsn := flag.String("dsn", "./db.sqlite", "sqlite3 DSN")
    slackWebhookURL := flag.String("slack-webhook-url", "https://hooks.slack.com/services/example", "slack webhook URL for notifications")

    flag.Parse()

    logger := log.New(os.Stdout, "", log.LstdFlags|log.Llongfile)

    db, err := sql.Open("sqlite3", *dsn)
    if err != nil {
        logger.Fatal(err)
    }
    defer db.Close()

    app := &application{
        logger:  logger,
        service: &service.Service{DB: db, SlackWebhookURL: *slackWebhookURL},
    }

    mux := flow.New()
    mux.HandleFunc("/register", app.registerUserHandler, "POST")

    logger.Print("starting server on :3000")
    err = http.ListenAndServe(":3000", mux)
    logger.Fatal(err)
}
File: cmd/api/handlers.go
package main

import (
    "errors"
    "net/http"

    "example.com/internal/service"
)

func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
    var input service.RegisterUserInput

    err := app.decodeJSON(r.Body, &input)
    if err != nil {
        app.badRequest(w, r, err)
        return
    }

    err = app.service.RegisterUser(&input)
    if err != nil {
        if errors.Is(err, service.ErrFailedValidation) {
            app.failedValidation(w, r, input.ValidationErrors)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

Hopefully you get the rough idea.

Essentially, our service layer contains a Service.RegisterUser() method which executes all the validation checks, business logic and SQL queries related to registering a user.

The expected input to this method is the simple, standard, service.RegisterUserInput Go struct.

And in our application layer's registerUserHandler() handler we can decode the JSON request body directly into that struct and pass it on the service layer, handling any returned errors as necessary.

The pros and cons

In terms of benefits, there are quite a lot of nice things about this pattern:

  • It's fairly simple. The number of mental hoops to jump through when reading the code is relatively low. You don't have to dig through lots of packages or abstractions to follow what the code is doing — meaning it's relatively easy for newcomers to your project to understand (or even yourself after a long break).

  • The separation of concerns keeps our registerUserHandler() code primarily focused on reading and writing HTTP requests and responses.

  • The code in the service layer can be reused by other applications. For example, we could create a CLI application under cmd/cli with a task that also calls the Service.RegisterUser() method.

  • This one is more personal, but I find it easier to reason about my business logic and write the code for it when the input is a well-defined Go struct with the correct types (rather than a more 'messy' input like a JSON string or HTML-encoded form data).

  • It's really practical for APIs and web applications. You can parse JSON or HTML form data from a request body directly into the service.RegisterUserInput struct in your handlers, and then pass that struct to the service layer for processing. You don't need to create interim types in your handlers to hold the decoded request data, or copy data from one struct to another.

  • Methods in the service layer can potentially return validation errors from multiple points in the code, and you can deal with them all just once in your handler. For example, if our user INSERT failed because we tried to insert a record with a duplicate username, then we could return a "username is already taken" validation error from our service layer in addition to the pre-INSERT validation checks.

  • Working with database transactions is easy. If we wanted to execute multiple SQL statements as part of registering a user in a single transaction, we could initialize the sql.TX, execute all the necessary statements, and commit the transaction all within our Service.RegisterUser() method. We don't need to pass the sql.TX around to a bunch of different places in our codebase.

  • If you want to test only your application layer logic only, this pattern lends itself nicely to creating an interface type that describes the methods on the service.Service struct, which you can then satisfy with a mock implementation.

But it's not perfect, and there are also a few downsides:

  • When you are looking at the code for your handlers, you can't immediately see what the expected inputs are. You have to navigate to the service package and look at the fields of the service.RegisterUserInput struct. With most modern text editors this is just one click away, but it still introduces a bit of 'obscurity' and feels less than ideal to me.

  • Not having a separate abstraction for the database logic makes it harder to swap out one database for another in the future (say moving from SQLite to PostgreSQL).

  • You can't easily mock the database calls during tests. Personally I tend to prefer using a test instance of an actual database for testing, so I don't find this too much of a drawback most of the time. But if you need to mock the database (i.e. to speed up test runtime, or because it's a hard requirement from a client) then this pattern doesn't really suit that.

  • Lastly, SQL queries which use database/sql and the Query() method to return multiple rows of data are quite verbose. These queries can take up a lot of visual space and add clutter to the service layer methods — which ultimately starts to reduce the scannability of the code. Using jmoiron/sqlx or blockloop/scan can be a big help here.

But overall I like this pattern. I've used it a lot over the past 3-4 years and have found it pragmatic and practical for small-to-medium sized projects. So long as you don't need to mock your database calls, you might want to consider it as an alternative to the more common 2-layer handler-repository pattern (where business logic is in your handlers), or a simpler alternative to a more complicated 3-layer handler-service-repository pattern.