Skip to content

Application Container

The ApplicationContainer handles the app life cycle letting you provide dependencies applying the control of inversion principle.

The dependencies have access to the application life cycle at:

  • Start: Once that all dependencies are registered, the app is executed by .Run() method, here is when the app is set to start status and all dependencies OnStart hooks are executed.
  • Stop: This is the last app status, when a signal termination is received the dependencies OnStop hooks are executed.

Please check the example application.

Provider

The provider structs are where the instances to provide are created and wired. Each provider have two different fields annotated with Go tags in order to define its functionality. The tags could be:

torpedo.di:"provide"

The field annotated with this tag is marked as provider to be bound in other provider which requires its instance. Your provider should implement the method Provide() where all fields tagged as provide MUST be initialized.

torpedo.di:"bind"

The field annotated with bind tag will have injected automatically the provided instance of its data type.

When you need to provide more than one instance of the same data type the provide tag could contain an attribute name.

1
2
3
4
5
6
7
8
type LoggerProvider struct {
    app.BaseProvider

    // instance to be provided
    defaultInstance *slog.Logger `torpedo.di:"provide"`
    instance        *slog.Logger `torpedo.di:"provide,name=LOGGER"`

}

The providers must implement the interface IProvider but for simplicity can extend the base struct named as app.BaseProvider which has implemented all the required methods as no operation mode. So, the developer only needs to overwrite at least the Provide() method.

Provide Singleton vs Builder function

The previous LoggerProvider example illustrates how to create a Singleton instance of slog.Logger. So, each Provider that contains a bind of *slog.Logger will have injected the same logger instance, which for a logger could be ok, but what happens if we would like to inject different instances per bound dependency. Here is when the Builder function is useful. This function will be called in order to build a new instance of the required object. The previous example can be modified as:

type LoggerProvider struct {
    app.BaseProvider

    // singletonInstance is a pointer to a singleton instance of slog.Logger
    singletonInstance *slog.Logger `torpedo.di:"provide"`

    // builderInstance Instance builder function that returns a pointer to a new instance of slog.Logger 
    builderInstance func(c app.IContainer) *slog.Logger `torpedo.di:"provide,name=LOGGER"`

}

// Provide this method sets the declared providers.
func (p *LoggerProvider) Provide(c app.IContainer) error {

    p.singletonInstance = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))

    p.builderInstance = func(c app.IContainer) *slog.Logger {
        return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))    
    }

    return nil
}

For instance a good example could be a HTTP Server:

package dependency

import (
    "github.com/darksubmarine/torpedo-lib-go/app"
    "github.com/darksubmarine/torpedo-lib-go/conf"
    "log/slog"

    "fmt"
    "io"
    "net/http"
)

type HttpServerProvider struct {
    app.BaseProvider
    cfg conf.Map

    // instance to be provided
    mux *http.ServeMux `torpedo.di:"provide,name=HTTPSERVER"`
}

func NewHttpServerProvider(config conf.Map) *HttpServerProvider {
    return &HttpServerProvider{cfg: config}
}

func (p *HttpServerProvider) Provide(c app.IContainer) error {
    p.mux = http.NewServeMux()
    p.mux.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "pong!")
    })

    return nil
}

func (p *HttpServerProvider) OnStart() func() error {
    return func() error {
        go func() {
            port := p.cfg.FetchIntP("port")
            if err := http.ListenAndServe(fmt.Sprintf(":%d", port), p.mux); err != nil {
                panic(fmt.Sprintf("error starting HTTP server at %d with error %s", port, err))
            }
        }()

        return nil
    }
}

Once that we have the HTTP server provider we could create new provider to add an endpoint:

package dependency

import (
    "github.com/darksubmarine/torpedo-lib-go/app"
    "io"

    "net/http"
)

type HelloProvider struct {
    app.BaseProvider

    mux    *http.ServeMux `torpedo.di:"bind,name=HTTPSERVER"`
}

func NewHelloProvider() *HelloProvider { return new(HelloProvider) }

func (p *HelloProvider) Provide(c app.IContainer) error {
    p.mux.HandleFunc("/hello", p.sayHello)

    // in this case we won't be providing anything...
    // only using the Provide method to inject a hello controller
    // as part of the mux server
    return nil
}

func (p *HelloProvider) sayHello(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "Hello, HTTP!\n")
}

Running the application container

The main application must be initialized with an instance of IApp like:

package main

import (
    "github.com/darksubmarine/torpedo-lib-go/app"
    "github.com/darksubmarine/torpedo-lib-go/conf"
    "log/slog"
    "os"
)

func main() {

    // 1. App configuration
    config := conf.Map{}
    config.Add(3000, "port")

    // 2. Depdencies
    opts := app.ContainerOpts{Log: app.ContainerLogsOpts{W: os.Stdout, L: slog.LevelInfo}}

    application := app.NewApplicationContainer(opts)
    application.WithProvider(dependency.NewHttpServerProvider(config))
    application.WithProvider(dependency.NewHelloProvider())

    // 3. Run your application!
    application.Run()
}