{"id":20578321,"url":"https://github.com/mikestefanello/hooks-example","last_synced_at":"2025-04-14T19:12:17.047Z","repository":{"id":65552066,"uuid":"534458500","full_name":"mikestefanello/hooks-example","owner":"mikestefanello","description":"Example of a modular monolithic codebase in Go using hooks and dependency injection.","archived":false,"fork":false,"pushed_at":"2022-09-11T00:26:05.000Z","size":24,"stargazers_count":18,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-06-20T11:12:00.413Z","etag":null,"topics":["dependencies","dependency-injection","example","example-app","go","golang","hooks","modular","modularization","module","monolith"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mikestefanello.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-09-09T01:42:05.000Z","updated_at":"2024-04-17T15:29:17.000Z","dependencies_parsed_at":"2023-01-29T00:30:27.000Z","dependency_job_id":null,"html_url":"https://github.com/mikestefanello/hooks-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikestefanello%2Fhooks-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikestefanello%2Fhooks-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikestefanello%2Fhooks-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikestefanello%2Fhooks-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mikestefanello","download_url":"https://codeload.github.com/mikestefanello/hooks-example/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224880346,"owners_count":17385370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dependencies","dependency-injection","example","example-app","go","golang","hooks","modular","modularization","module","monolith"],"created_at":"2024-11-16T06:12:24.915Z","updated_at":"2024-11-16T06:12:25.421Z","avatar_url":"https://github.com/mikestefanello.png","language":"Go","readme":"# Hooks (examples) - A modular monolithic approach\n\n## Overview\n\nAside from just providing usage examples for the [hooks](https://github.com/mikestefanello/hooks) library, this is an exploration in modular monolithic architectural patterns in Go by leveraging both [hooks](https://github.com/mikestefanello/hooks) and [do](https://github.com/samber/do) _(for dependency injection)_.  It's recommended you review and understand these libraries prior to reviewing this repository. [Do](https://github.com/samber/do) is not required to achieve the pattern illustrated in this application, but I find it to be a very helpful and elegant approach.\n\nI'm by no means advocating (at this time) for this specific approach but rather using this as an experiment and place to iterate with these ideas. I have had a lot of success with modular monoliths with languages and frameworks prior to learning Go and I haven't come across any 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.\n\nThe overall goals of this approach are:\n1) Create self-contained _modules_ that represent segments of business logic.\n2) 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).\n3) Modules should be able to be added and removed without having to touch the _core_ codebase at all.\n\n## Repo structure\n\nBelow describes the repo structure and is just a proposed idea for effective, clear organization, but there's no requirement to follow this.\n\n```\nhooks-example/\n├─ modules/         # Modules that each represent a unit of independent business logic \n│  ├─ analytics/\n│  ├─ todo/\n├─ pkg/             # General-purpose, non-dependency packages which can be used across the application \n│  ├─ app/\n├─ services/        # Services which are auto-registered as dependencies\n│  ├─ cache/\n│  ├─ config/\n│  ├─ web/\n├─ main.go\n```\n\n### Modules\n\nSee the `func init()` within the primary, self-named _.go_ file of each module to understand how the module auto-registers itself with the application.\n\n- `modules/todo`: Provides a very simple todo-list implemenation 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 prior to saving and react when they are saved.\n- `modules/analytics`: Provides bare-bones analytics for the application including the number of web requests received and the amount 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 in order to track entities.\n\n## Hooks\n\n### Dispatchers\n\n- `pkg/app`\n  - `HookBoot`: Indicates that the application is booting and allow dependencies to be registered across the entire application via `*do.Injector`.\n- `services/web`\n  - `HookBuildRouter`: Dispatched when the web router is being built which allows listeners to register their own web routes and middleware.\n- `modules/todo`\n  - `HookTodoPreInsert`: Dispatched prior to inserting a new _todo_ which allows listeners to make any required modifications.\n  - `HookTodoInsert`: Dispatched after a new _todo_ is inserted.\n- `modules/analytics`\n  - `HookAnalyticsUpdate`: Dispatched when the analytics data is updated.\n\n### Listeners\n\n- `HookBoot`\n  - `services/cache`: Registers a cache backend as a dependency.\n  - `services/config`: Registers configuration as a dependency.\n  - `services/web`: Registers a web server as a dependency.\n  - `modules/analytics`: Registers analytics service and HTTP handler as dependencies.\n  - `modules/todo`: Registers todo service and HTTP handler as dependencies.\n- `HookBuildRouter`\n  - `modules/analytics`: Registers web route and tracker middleware for analytics.\n  - `modules/todo`: Registers web routes for todos.\n- `HookTodoInsert`\n  - `modules/analytics`: Increments analytics entity count when todos are created.\n\n## Boot process and registration\n\nBelow is an attempt to illustrate how the entire application self-registers starting from a single hook that is invoked.\n\n### Code\n\n```go\nfunc main() {\n  i := app.Boot()\n  \n  server := do.MustInvoke[web.Web](i)\n  _ = server.Start()\n}\n```\n\n### Walkthrough\n\n```\nmain.go/              app.Boot()\n├─ pkg/app.go:        [Dispatch] HookBoot \n├─ services/cache.go  ├─  Register dependency: *cache.Cache\n├─ services/config.go ├─  Register dependency: *config.Config\n├─ services/web.go    ├─  Register dependency: *web.Web\n├─ modules/analytics: ├─  Register dependency: *analytics.Service\n├─ modules/analytics: ├─  Register dependency: *analytics.Handler\n├─ modules/todo:      ├─  Register dependency: *todo.Service\n├─ modules/todo:      ├─  Register dependency: *todo.Handler\n\nmain.go/              server := do.MustInvoke[web.Web](i)\n├─ services/web.go:   ├─  Initialize *web.Web\n├                     ├───  Initialize *config.Config\n├                     ├───  [Dispatch] HookRouterBuild\n├─ modules/analytics:      ├─  Register web routes and middleware\n├                          ├───  Initialize *analytics.Handler\n├                          ├─────  Initialize *analytics.Service\n├                          ├───────  Initialize *cache.Cache  \n├─ modules/todo:           ├─  Register web routes\n├                          ├───  Initialize *todo.Handler\n├                          ├─────  Initialize *todo.Service\n├                          ├───────  Initialize *cache.Cache  \n```\n\n## Imports\n\nIt's important to note that if you want a module or service to self-register, it must be imported. This is why you see this in `main.go`:\n\n```go\n// Services\n_ \"github.com/mikestefanello/hooks-example/services/cache\"\n_ \"github.com/mikestefanello/hooks-example/services/config\"\n\"github.com/mikestefanello/hooks-example/services/web\"\n// Modules\n_ \"github.com/mikestefanello/hooks-example/modules/analytics\"\n_ \"github.com/mikestefanello/hooks-example/modules/todo\"\n```\n\nThis is needed to ensure that `init()` executes in each package which is what they are using to listen to hooks.\n\n### Logs\n\nTo help illustrate the app boot process:\n\n```go\n2022/09/09 15:50:22 hook created: boot\n2022/09/09 15:50:22 registered listener with hook: boot\n2022/09/09 15:50:22 registered listener with hook: boot\n2022/09/09 15:50:22 hook created: router.build\n2022/09/09 15:50:22 registered listener with hook: boot\n2022/09/09 15:50:22 hook created: todo.pre_insert\n2022/09/09 15:50:22 hook created: todo.insert\n2022/09/09 15:50:22 registered listener with hook: boot\n2022/09/09 15:50:22 registered listener with hook: router.build\n2022/09/09 15:50:22 hook created: analytics.update\n2022/09/09 15:50:22 registered listener with hook: boot\n2022/09/09 15:50:22 registered listener with hook: router.build\n2022/09/09 15:50:22 registered listener with hook: todo.insert\n2022/09/09 15:50:22 dispatching hook boot to 5 listeners (async: false)\n2022/09/09 15:50:22 dispatch to hook boot complete\n2022/09/09 15:50:22 registered 7 dependencies: [*analytics.Handler *cache.Cache *config.Config *web.Web *todo.Service *todo.Handler *analytics.Service]\n2022/09/09 15:50:22 dispatching hook router.build to 2 listeners (async: false)\n2022/09/09 15:50:22 dispatch to hook router.build complete\n2022/09/09 15:50:22 registered 5 routes: [GET_/ GET_/todo GET_/todo/:todo POST_/todo GET_/analytics]\n```\n\n### Module registration\n\nBelow is the code used by the `analytics` module to register itself:\n\n```go\nfunc init() {\n    // Provide dependencies during app boot process\n    app.HookBoot.Listen(func(e hooks.Event[*do.Injector]) {\n        do.Provide(e.Msg, NewAnalyticsService)\n        do.Provide(e.Msg, NewAnalyticsHandler)\n    })\n\n    // Provide web routes\n    web.HookBuildRouter.Listen(func(e hooks.Event[*echo.Echo]) {\n        h := do.MustInvoke[Handler](do.DefaultInjector)\n        e.Msg.GET(\"/analytics\", h.Get)\n        e.Msg.Use(h.WebRequestMiddleware)\n    })\n\n    // React to new todos being inserted\n    todo.HookTodoInsert.Listen(func(e hooks.Event[todo.Todo]) {\n        h := do.MustInvoke[Service](do.DefaultInjector)\n        if err := h.IncrementEntities(); err != nil {\n            log.Error(err)\n        }\n    })\n}\n```\n\n## Optional independent binaries\n\nIt is possible to create separate entrypoints that only register one or some of your modules, allowing for a monolithic codebase that could be used to create separate applications/services.\n\nFor example, in `main.go`, simply remove the import `_ \"github.com/mikestefanello/hooks-example/modules/analytics\"` and the application will run without the `analytics` modules (and everything within it).\n\n## Run the application\n\n`go run main.go`\n\n### Endpoints\n\n_NOTE:_ Data created is stored in memory and will be lost when the application restarts.\n\n- `GET /`: Hello world\n- `GET /todo`: Get all _todos_\n- `GET /todo/:todo`: Get a _todo_ by ID\n- `POST /todo`: Create a todo\n- `GET /analytics`: Get analytics\n\n## Downsides\n\nNothing is without downsides and this approach certainly has them. It lacks overall explicitness by hiding details within hook listeners and by injecting all dependencies inside a single container. It could make understanding and debugging the codebase harder than one following a very straight-forward approach, especially since you lose some power of your IDE. This certainly goes a bit against the overall philosophy of Go itself. It's also hard to tell how well this would scale with a large codebase and even with multiple development teams.\n\nBut there are pros, in my opinion. I'll leave it to the reader to make their own judgements and I encourage you to share them here.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikestefanello%2Fhooks-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmikestefanello%2Fhooks-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikestefanello%2Fhooks-example/lists"}