Skip to content

Latest commit

 

History

History
95 lines (73 loc) · 5.9 KB

ARCHITECTURE.md

File metadata and controls

95 lines (73 loc) · 5.9 KB

Service Architecture and Design

The Bastet service provides a REST layer to interact with events, allowing the client to perform Create, Read, Update and Delete on them. It follows clean architecture principles to make developing, maintaining and debugging the service easier. This document provides an overview of the major components, how they fit together, implementation details, and some design decisions.

App Structure

├── Dockerfile
├── Makefile
├── README.md
├── cmd
│   └── bastet
│       └── main.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── internal
│   ├── config
│   │   └── config.go
│   ├── core
│   │   ├── errors.go
│   │   ├── event.go
│   │   ├── operations.go
│   │   ├── repository.go
│   │   └── service.go
│   ├── repository
│   │   ├── migrations
│   │   │   ├── 000001_create_events_table.down.sql
│   │   │   └── 000001_create_events_table.up.sql
│   │   ├── operations.go
│   │   ├── repository.go
│   │   ├── sqlc
│   │   │   ├── db.go
│   │   │   ├── models.go
│   │   │   ├── querier.go
│   │   │   └── queries.sql.go
│   │   └── utils.go
│   └── server
│       ├── event.go
│       ├── operations.go
│       ├── response.go
│       └── server.go
└── sql
    ├── init.sql
    ├── queries.sql
    ├── schema.sql
    └── sqlc.yaml
  1. ./Dockerfile containerizes the app while docker-compose.yml orchestrates the containers.
  2. ./Makefile provides a user friendly layer on some typical commands run on the repo.
  3. ./cmd/bastet/main.go provides the main entrypoint of the service. The main function serves only as an orchestrator, instantiating the relevant components of the app and injecting them between themselves as needed.
  4. ./internal is a Go specific folder that encapsulates application specific code, restricting its imports into different projects.
  • ./internal/config defines the configuration needed for the app, database and server.

  • ./internal/core represents the main business use case of the app, creates the relevant abstractions to perform them, disregarding implementation details to the lower level layers. It does contain business logic and validation.

  • ./internal/repository contains the repository (database) implementation, in this case for Postgres, heavily based on the SQLC tool. Migrations are included here.

  • ./internal/server represents the REST layer of the service for the client to communicate.

  1. ./sql contains the SQLC YAML definitions as well as the schema and queries.

Repository layer

The Repository layer will implement the core.Repository interface so it can be injected into the service. As such, it needs to Create, Read, Update and Delete events from whatever database is chosen.

The choice in this case was Postgres, heavily aided by SQLC, as it makes developing fast and easy, generating relevant code. We wrap around it though, injecting the SQLC Querier into the Repository, as it makes it cleaner and hides the SQLC implementation details from the Postgres repository itself.

There's an Event model defined here to avoid a dependency on the core Model and to properly manage some attributes, like CreatedAt, which are not present in the core model of the app.

Due to dependency injection and the core.Repository abstraction, if we were to change the database, it would be as simple as creating a new implementation with the new one, and instantiating it in main.

Service layer

Overall, the idea is to encapsulate the business logic in the core package (including validation and errors), while creating the necessary abstractions for the service to work. In this case, the only abstraction is the Repository interface (as it's the only 3rd party tool we need to the app to run), hiding the implementation details and delegating said responsibility to the relevant packages.

A Service interface is also defined to clarify what the service needs to do and to be abstracted from the Server.

A small caveat, theres a direct dependency on the slog package to log rather than hide it over a Logger interface to avoid over-engineering, as logging is something that doesn't usually change and the implementation of the interfaces can be tricky and time consuming.

As such, the Repository is inserted into the Service, and will be called upon to perform the CRUD operations when relevant, after all the validations and parsing on the service has ocurred. Once the data is gathered, it will be returned to the next layer.

Server layer

The Service is then injected into the server, which will parse HTTP requests, call the service to perform the operations, and send the relevant responses. The logic of parsing errors is also heavily based on the business use case errors defined in the core package.

An slog Logger object is also injected into it to provide access to necessary logs such as request IDs, timestamps, cookies, or whatever HTTP-related information we need to save.

Tooling

All tools are free and open source.

SQLC is the tool of choice as a code generation tool for database operations as it's schema first and significantly reduces development time. Postgres is the database choice, along with the pgx library, due to performance.

chi is chosen as the router as it's a minimal library, battle tested, with an intuitive API.

envconfig and godotenv are chosen to manage configuration through environment variables, the recommended way of inserting secrets (or config) into containers.

slog is chosen as the structured logging package as it's going to be added to the standard library when go 1.21 is released.