Example of a modular monolithic codebase in Golang using hooks and dependency injection

Golang Modular Monolithic

Hooks (examples) – A modular monolithic approach

Overview

Aside from just providing usage examples for the hooks library, this is an exploration of modular monolithic architectural patterns in Go by leveraging both hooks and do (for dependency injection). It’s recommended you review and understand these libraries before reviewing this repository. Do is not required to achieve the pattern illustrated in this application, but I find it to be a very helpful and elegant approach.

I’m by no means advocating (at this time) for this specific approach but instead using this as an experiment and place to iterate with these ideas. I had a lot of success with modular monoliths with languages and frameworks before learning Golang, and I haven’t encountered similar patterns within the Go ecosystem. While microservices have become more prominent, a modular monolith can not only be a better choice in certain circumstances but if done well, can make transitioning to microservices easier.

The overall goals of this approach are:

  1. Create self-contained modules that represent segments of business logic.
  2. Avoid any patterns that reach across the codebase (ie, the entrypoint being used to initialize all dependencies, a router that initializes all handlers and routes, etc).
  3. Modules should be able to be added and removed without having to touch the core codebase at all.

Repo structure

Below is the repo structure and is just a proposed idea for an effective, clear organization, but there’s no requirement to follow this.

hooks-example/
├─ modules/         # Modules that each represent a unit of independent business logic 
│  ├─ analytics/
│  ├─ todo/
├─ pkg/             # General-purpose, non-dependency packages which can be used across the application 
│  ├─ app/
├─ services/        # Services which are auto-registered as dependencies
│  ├─ cache/
│  ├─ config/
│  ├─ web/
├─ main.go

Golang Modules

See the func init() within the primary, self-named .go file of each module to understand how the module auto-registers itself with the application.

  • modules/todo: Provides a simple todo-list implementation with a Todo model, a service to interact with todos as a registered dependency, an HTTP handler as a registered dependency, some JSON REST endpoints, and hooks to allow other modules to alter todos before saving and react when they are saved.
  • modules/analytics: Provides bare-bones analytics for the application, including the number of web requests received and the number of entities created. Included is an Analytics model, a service to interact with analytics as a registered dependency, an HTTP handler as a registered dependency, middleware to track requests, a GET endpoint to return analytics, a hook to broadcast updates to the analytics, a listener for todo creation to track entities.

Golang Hooks

Dispatchers

  • pkg/app
    • HookBoot: Indicates that the application is booting and allow dependencies to be registered across the entire application via *do.Injector.
  • services/web
    • HookBuildRouter: Dispatched when the web router is being built which allows listeners to register their own web routes and middleware.
  • modules/todo
    • HookTodoPreInsert: Dispatched prior to inserting a new todo which allows listeners to make any required modifications.
    • HookTodoInsert: Dispatched after a new todo is inserted.
  • modules/analytics
    • HookAnalyticsUpdate: Dispatched when the analytics data is updated.

Listeners

  • HookBoot
    • services/cache: Registers a cache backend as a dependency.
    • services/config: Registers configuration as a dependency.
    • services/web: Registers a web server as a dependency.
    • modules/analytics: Registers analytics service and HTTP handler as dependencies.
    • modules/todo: Registers todo service and HTTP handler as dependencies.
  • HookBuildRouter
    • modules/analytics: Registers web route and tracker middleware for analytics.
    • modules/todo: Registers web routes for todos.
  • HookTodoInsert
    • modules/analytics: Increments analytics entity count when todos are created.

Boot process and registration

Below is an attempt to illustrate how the entire application self-registers starting from a single hook that is invoked.

Golang Code

func main() {
  i := app.Boot()
  
  server := do.MustInvoke[web.Web](i)
  _ = server.Start()
}

Looking for Golang Jobs