18 min read

Config Chronicles

A web app may require numerous parameters to be set, making a robust configuration mechanism essential. Today, we'll tackle this requirement head-on.
Configuration gears

In the previous chapters of our path towards building Plykan, our Kanban board application using Go, we've covered essential topics like setting up the project and understanding RESTful routing and logging. Now, as we continue our quest to develop a robust application, we encounter a new challenge: configuration.

In the early stages of development, it's common to hardcode values for server addresses, ports, database connections, and other parameters. However, as your application progresses and moves from development to testing and production environments, this approach becomes impractical. This is where the need for a well-structured configuration mechanism becomes a necessity.

Configuration defines your application's behavior, enabling you to adjust its parameters to suit various environment-specific requirements. Whether you're deploying on your local machine for development, on a staging server for testing, or in a production environment to serve users, you'll want the ability to tweak settings without modifying your code.

In this chapter, we'll explore multiple ways to configure your application, from simple command-line flags to environment variables and even advanced configuration libraries like spf13/viper. You'll discover how to shift from hardcoding values to building a structured configuration system that enhances both maintainability and organization.

But it's not just about learning how to configure Plykan. What you'll discover here extends far beyond this project. We'll dive into the principles of good configuration practices, including the twelve-factor methodology and the use of dependency injection to seamlessly provide configuration data to various parts of your application.

So, if you've ever wondered how to make your Go applications adaptable, maintainable, and ready for production, you're in the right place. Let's start.

Command Line-Based Flags


One of the fundamental aspects of configuration in Go is the use of command-line flags. Flags are a convenient way to pass runtime parameters to your application, allowing you to customize its behavior without modifying the code.

port := flag.String("port", ":8080", "Web server port")

port: This is a pointer to a string where the flag's value will be stored.

  • port: This is the flag's name, which you'll use when running your application from the command line. It's preceded by either a single hyphen - or a double hyphen --. In this case, we use a single hyphen to denote a short flag. However, Go's flag package is flexible and accepts both single and double dashes for long options. This means that both `--string foo" and -string="foo" are accepted styles for specifying flags and their values.
  • :8080: This is the default value assigned to the flag.
  • Web server port: This is a brief description of the flag's purpose, which is used to generate help messages.

Setting Default Values

Setting default values for flags is essential to ensure your application functions correctly, even in cases where users don't explicitly specify these flags. For instance, many database engines default to using specific port numbers, such as 5432 for PostgreSQL and 27017 for MongoDB to name a few. In the example above, ":8080" is the default value for the port flag. If a user doesn't provide a value for it when running the application, it will default to 8080.

Type Conversion

By default, the flag.String function is used to create a string flag. However, you can easily convert the flag's value to other types as needed. For instance, if you prefer to pass a flag of type 8080 rather than :8080, and your application requires an integer value, you can achieve this by converting the string.

port := flag.Int("port", 8080, "Web server port")`

Now, the port flag is of type int, and Go will handle the conversion from the command-line argument to an integer automatically. You can work with various types of flags, such as boolean, float64, int, int64, string, uint, and uint64, depending on your application's needs. For a comprehensive list and detailed information on each flag type, you can refer to the official Go documentation: Go Flag Package Documentation.

As an example, below, we'll illustrate how to use flags with boolean and float64 types. Keep in mind that the process is similar for other types; you get the idea.

Boolean

verbose := flag.Bool("verbose", false, "Enable verbose mode")

Float64

threshold := flag.Float64("threshold", 0.5, "Threshold value")

Peculiarities of Boolean Flags

Boolean flags in Go have some peculiarities. When you define a boolean flag, you can set it to true or false on the command line, and Go will handle it accordingly. For example:

go run main.go -verbose=true

Or:

go run main.go -verbose=false

However, if you omit the flag's value, it will default to false. Boolean flags in Go's flag package has this behavior if you specify a boolean flag on the command line without explicitly setting it to true or false, it will be treated as true. So, both go run main.go -verbose=true and go run main.go -verbose will have the same effect, setting the "verbose" flag to true.

flag.[type]() vs. flag.[type]Var()

In Go, when you're working with flags, you'll encounter functions like flag.[type]() and flag.[type]Var(). The key difference lies in how you handle and store the parsed flag values.

flag.[type]() functions return a pointer to the variable where the flag value is stored. For instance, flag.Int() returns a pointer to an int variable. You can use this approach to work with the flag value as a local variable within a specific function or scope. For example, you can use flag.Int() to parse an integer value:

count := flag.Int("count", 0, "Count value")

On the other hand, the flag.[type]Var() functions empower you to furnish a pointer to a variable of the designated type. These functions seamlessly assign the parsed flag value directly to the variable whose address you've supplied. Let's illustrate this with an example with flag.IntVar().

var count int
flag.IntVar(&count, "count", 0, "Count value")

The same applies to other flag types, including strings, booleans, floats, and duration-related flag parsing functions.

Help Messages

One advantage of the flag package is its built-in support for generating help messages. When users run your application with the -h or --help flag, Go will display a list of available flags along with their descriptions. This feature helps you make your application user-friendly and self-documenting.

In this section, we've laid the foundation for configuring your Plykan application through command-line flags. Next, we'll explore another configuration mechanism: environment variables.

Environment Variables

When it comes to configuring applications, environment variables offer a flexible and widely adopted approach. They also provide a means of customizing your application's behavior without modifying code.

Let's consider the port configuration as an example. Instead of reading a flag like here.

port := flag.Int("port", 8080, "Web server port")

You can easily read this value from a system envar following this approach

portStr := os.Getenv("PLYKAN_WEB_PORT")
port, err := strconv.Atoi(portStr)
if err != nil {
    // Error handling
}

To configure Plykan's web port setting manually through environment variables, you can set the PLYKAN_WEB_PORT variable in your shell initialization file or for the current session. For example, in a Linux or macOS environment, you can export the variable.

$ export PLYKAN_WEB_PORT=":8080"

Environment variables give us a way to configure your application, but it comes with some drawbacks:

  • Lack of Type Conversion: Unlike flags, environment variables are usually read as strings, requiring manual type conversion, as demonstrated in the example above strconv.Atoi to parse a string value into an integer.
  • No Built-in Help Mechanism: Environment variables lack the built-in help mechanism that the flag library offers. This can make it less user-friendly when users are trying to understand which variables are available and their purpose.

The Twelve-Factor App Manifest

Before we dive deeper into a solution that combines the benefits of both flags and environment variables, let's briefly touch on the Twelve-Factor App manifesto. This manifesto outlines best practices for building modern, scalable, and maintainable web applications. One of its principles emphasizes the use of environment variables for configuration, promoting the separation of configuration from code.

A Better Approach: Combining Flags and Environment Variables

So, what's the best approach to a configuration that combines the benefits of both flags and environment variables? Consider this:

Manually export environment variables for the session or include them in your shell initialization files as we did above. Later, when launching your Go application, pass the environment variable as a flag, like this:

$ go run main.go -port=$PLYKAN_WEB_PORT

By following this approach, you can enjoy the advantages of the flag libraries, such as automatic type conversion and built-in help messages, while still having the flexibility to configure your application using environment variables.

This strategy also aligns with the Twelve-Factor App manifesto's recommendations for separating configuration from code and making your application more adaptable to various deployment environments.

Before we explore more advanced configuration techniques, we'll begin with a necessary code restructuring. Currently, our handlers are defined within the main function, but we're planning to move them to a dedicated package. This organizational change will pave the way for a more modular and maintainable codebase. In the next section, we'll delve into the details of this refactoring process.

Introducing the Router

To improve our code's organization and modularity, we're going to introduce a Router structure in the internal/web package. The Router will manage our application's routes while also handling common functionalities such as logging and configuration. This pattern, where specific functionality is encapsulated within structs along with common features like logging and configuration, will be a recurring thing as we continue developing our Kanban board.

package web

import (
	"github.com/gorilla/mux"
	"github.com/solutioncrafting/plykan/internal/controller"
	"github.com/solutioncrafting/plykan/internal/log"
)

type Router struct {
	name   string
	log    log.Logger
	router *mux.Router
}

func NewRouter(name string, log log.Logger) *Router {
	r := &Router{
		name:   name,
		log:    log,
		router: mux.NewRouter(),
	}

	r.SetupRoutes()

	return r
}

func (r *Router) SetupRoutes() {
	// Board controller
	bc := controller.NewBoardController(r.log)

	// Board routes
	r.router.HandleFunc("/boards", bc.BoardsIndexH).Methods("GET")
	r.router.HandleFunc("/boards/new", bc.NewBoard).Methods("GET")
	r.router.HandleFunc("/boards", bc.CreateBoard).Methods("POST")
	r.router.HandleFunc("/boards/{id}", bc.ShowBoard).Methods("GET")
	r.router.HandleFunc("/boards/{id}/edit", bc.EditBoard).Methods("GET")
	r.router.HandleFunc("/boards/{id}", bc.UpdateBoard).Methods("PUT")
	r.router.HandleFunc("/boards/{id}", bc.DeleteBoard).Methods("DELETE")

	// Special route for delete confirm, contingent on client's JS capabilities
	r.router.HandleFunc("/boards/{id}/confirm-delete", bc.DeleteConfirm).Methods("GET")
}

func (r *Router) Name() string {
	return r.name
}

func (r *Router) Log() log.Logger {
	return r.log
}

internal/web/router.go

Additionally, we've introduced a Server structure to handle server-related configurations. Here's a simplified representation of what the Server structure might look like:

package web

import (
	"context"
	"github.com/gorilla/mux"
	"github.com/solutioncrafting/plykan/internal/log"
	"golang.org/x/sync/errgroup"
	"net/http"
	"time"
)

type Server struct {
	name string
	port string
	log  log.Logger
	http.Server
	router *Router
}

func NewServer(name, port string, log log.Logger) *Server {
	return &Server{
		name:   name,
		log:    log,
		router: NewRouter("web-router", log),
	}
}

func (srv *Server) Start(ctx context.Context) error {
	srv.Server = http.Server{
		Addr:    srv.port,
		Handler: srv.Router(),
	}

	group, errGrpCtx := errgroup.WithContext(ctx)
	group.Go(func() error {
		srv.Log().Infof("%s started listening at %s", srv.Name(), port)
		defer srv.Log().Errorf("%s shutdown", srv.Name())

		err := srv.Server.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			return err
		}

		return nil
	})

	group.Go(func() error {
		<-errGrpCtx.Done()
		srv.Log().Errorf("%s shutdown", srv.Name())

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := srv.Server.Shutdown(ctx); err != nil {
			return err
		}

		return nil
	})

	return group.Wait()
}

func (srv *Server) Name() string {
	return srv.name
}

func (srv *Server) Log() log.Logger {
	return srv.log
}

func (srv *Server) Router() *mux.Router {
	return srv.router.router
}

internal/web/server.go

The Server struct encapsulates server-related configurations, including common features such as logging and configuration settings.

This introduces a new approach to managing the web server's lifecycle. Instead of launching the server directly from the main function, we've introduced a Start(ctx context.Context) method within the Server struct.

Here's how our updated main function accommodates these changes.

package main

import (
	"context"
	"flag"
	l "github.com/solutioncrafting/plykan/internal/log"
	"github.com/solutioncrafting/plykan/internal/web"
)

func main() {
	port := flag.String("port", ":8080", "Web server port")
	logLevel := flag.String("log-level", "info", "Log level")

	log := l.NewLogger(*logLevel)

	server := web.NewServer("web-server", *port, log)

	err := server.Start(context.Background())
	if err != nil {
		log.Error(err)
	}
}

main.go

We configure Plykan's key parameters through Go's flag package. These parameters include the web server's port and the log level. Default values are given for both: :8080 for the port and info for the log level. It's essential to note that after defining these flags, we call flag.Parse() to parse the command-line arguments and update the flag values accordingly. Failing to add this call to Parse() will prevent the pointer variables from receiving values from the command line, causing them to store the default values instead.

To launch Plykan with custom configurations, you can use the following command:

go run main.go --port=<desired_port> --log-level=<desired_log_level>

An example of this:

go run main.go --port=:8081 --log-level=debug

Use of Getters and Setters

In Go, it's common to use exported properties in structs rather than non-exported properties with getters and setters. We've followed this approach for a specific purpose related to future interface implementation. Of course, there is nothing inherently wrong with favoring this higher level of encapsulation, and, in fact, it is often more prevalent in larger applications.

Not so DRY

You surely have noticed the redundancy in the definitions of our Server and Router structs, where they share common properties like a name and logger. While this redundancy exists in the current code, we acknowledge it as an area for improvement. Later in the series, we'll address this issue and aim for a more DRY (Don't Repeat Yourself) codebase.

In the next section, we'll transition from simple configuration variables to employing a more structured approach, maintaining the same fundamental concepts but enhancing the organization of our app's configuration.

From Simple Var Pointers to a Struct Config

We'll introduce the concept of a parent Config struct that encapsulates references to other configs, like server, log, and database, enabling efficient management of various aspects of the Plykan configuration.

First, let's take a closer look at the new Config package, located in internal/config. This package is responsible for handling Plykan's configuration. It defines a Config struct, which itself contains two embedded structs: Server and Log.

package config

import (
	"flag"
	"fmt"
)

type (
	Config struct {
		Web Server
		Log
	}

	Server struct {
		Host string
		Port int
	}

	Log struct {
		Level string
	}
)

internal/config/config.go

The Config struct serves as the central container for app configuration parameters. It embeds two other structs, Server and Log, which represent server-related and logging configurations, respectively.

To load config settings, we introduce a Load() function.

func Load() *Config {
	cfg := Config{}

	// Server
	flag.StringVar(&cfg.Web.Host, "web-host", "localhost", "Web server host")
	flag.IntVar(&cfg.Web.Port, "web-port", 8080, "Web server port")

	// Log
	flag.StringVar(&cfg.Log.Level, "log-level", "info", "Log level")

	return &cfg
}

internal/config/config.go

This Load() function initializes a Config struct and sets up the configuration parameters using Go's flag package. We define flags for the web server's host and port as well as the log level.

Now, let's revisit the main.go file to see how the usage of the Config package impacts our code.

package main

import (
	"context"
	"github.com/solutioncrafting/plykan/internal/config"
	l "github.com/solutioncrafting/plykan/internal/log"
	"github.com/solutioncrafting/plykan/internal/web"
)

func main() {
	cfg := config.Load()

	log := l.NewLogger(cfg.Log.Level)

	server := web.NewServer("web-server", cfg, log)

	err := server.Start(context.Background())
	if err != nil {
		log.Error(err)
	}
}

main.go

In this updated version of main.go, we've replaced the direct use of flag package variables with a call to config.Load(), which returns a fully configured Config struct. We then use this Config struct to set up the logger and create the web server instance. Similar to the logger, we inject the configuration as a dependency into the server.

As we did before to inject the logger into our Router, we now apply the same approach to inject the config into our Server entity, along with the logger.

package web

import (
	"context"
	"github.com/gorilla/mux"
	"github.com/solutioncrafting/plykan/internal/config"
	"github.com/solutioncrafting/plykan/internal/log"
	"golang.org/x/sync/errgroup"
	"net/http"
	"time"
)

type Server struct {
	name string
	cfg  *config.Config
	log  log.Logger
	http.Server
	router *Router
}

func NewServer(name string, cfg *config.Config, log log.Logger) *Server {
	return &Server{
		name:   name,
		cfg:    cfg,
		log:    log,
		router: NewRouter("web-router", log),
	}
}

func (srv *Server) Start(ctx context.Context) error {
	srv.Server = http.Server{
		Addr:    srv.cfg.Web.Addr(), // Updated to use configuration
		Handler: srv.Router(),
	}

	group, errGrpCtx := errgroup.WithContext(ctx)
	group.Go(func() error {
		srv.Log().Infof("%s started listening at %s", srv.Name(), srv.cfg.Web.Addr()) // Updated to use configuration
		defer srv.Log().Errorf("%s shutdown", srv.Name())

		err := srv.Server.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			return err
		}

		return nil
	})

	group.Go(func() error {
		<-errGrpCtx.Done()
		srv.Log().Errorf("%s shutdown", srv.Name())

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := srv.Server.Shutdown(ctx); err != nil {
			return err
		}

		return nil
	})

	return group.Wait()
}
// No changes after this

internal/web/server.go

In this updated code for the Server struct, we've made the following changes:

  • In the NewServer constructor function, we now accept a config.Config object as a parameter. This allows us to inject the configuration into the server during its creation.
  • In the Start method, we've replaced the server port with srv.cfg.Web.Addr(), which retrieves the server address from the configuration.

To showcase the usage of the new configuration approach, we will use the following command to run Plykan, specifying configuration values as needed.

go run main.go --web-host=localhost --web-port=8080 --log-level=debug

Viper

We are going to introduce a powerful library spf13/viper that enhances our configuration management capabilities. While our previous approach for configuration was functional, now, we'll transition to viper to harness its extensive features.

spf13/viper is a versatile configuration solution tailored for Go applications, including those following the 12-Factor app recommendations. It is intricately crafted to seamlessly integrate with your application, offering a comprehensive array of features, including:

  • Setting default values for configuration options.
  • Reading configuration from a variety of formats, including JSON, TOML, YAML, HCL, INI, envfile, and Java properties files.
  • Real-time watching and reloading of configuration files (optional).
  • Reading configuration from environment variables.
  • Retrieving configuration from remote systems like etcd or Consul and dynamically reacting to changes.
  • Parsing command line flags for configuration.
  • Reading configuration from buffers.
  • Explicitly setting configuration values.

The transition to viper offers several advantages, including a unified configuration source and the ability to read from various file formats and environment variables. This change aligns with our goals.

To achieve this transition smoothly, we've created an interface named Config within the internal/config package. This Config interface replicates the methods we are most interested in from viper library.

package config

import (
	"time"
)

// Config is the generic interface for configuration
type Config interface {
	GetString(key string) string
	GetBool(key string) bool
	GetInt(key string) int
	GetInt32(key string) int32
	GetInt64(key string) int64
	GetUint(key string) uint
	GetUint16(key string) uint16
	GetUint32(key string) uint32
	GetUint64(key string) uint64
	GetFloat64(key string) float64
	GetTime(key string) time.Time
	GetDuration(key string) time.Duration
	GetIntSlice(key string) []int
	GetStringSlice(key string) []string
	GetStringMap(key string) map[string]interface{}
	GetStringMapString(key string) map[string]string
	GetStringMapStringSlice(key string) map[string][]string
	GetSizeInBytes(key string) uint
}

We chose this approach because we value the benefits offered by spf13/viper, but we aim to avoid mandating this external dependency for every component in our project that needs configuration.

We've created a Config interface tailored to our application's needs. This interface is designed to accommodate multiple implementations, including one based on spf13/viper. Our SimpleConfig is one such implementation. This approach enhances the flexibility of our application, allowing entities within it to accept various implementations of the Config interface, including spf13/viper or any other that adheres to the same interface.

Additionally, it's worth noting that down the road, we have the option to create our own implementation of the methods within the interface. This could be advisable if we plan to reduce our external dependency on spf13/viper and intend to utilize only a limited subset of its capabilities.

With the Config interface defined, we can now create a concrete implementation, which, in our case, we'll name SimpleConfig. To do this, we need to modify our existing Config structure to adhere to the newly created interface. Essentially, we'll be refactoring our code to ensure that all the methods specified in the Config interface are implemented by our SimpleConfig structure.

package config

import (
    // Import necessary packages
    "github.com/spf13/viper"
)

// SimpleConfig is a struct that wraps viper and implements the Config interface.
type SimpleConfig struct {
    name   string
    v      *viper.Viper
    file   string
    search bool
    status map[time.Time]string
}

// NewSimpleConfig creates a new instance of SimpleConfig.
func NewSimpleConfig(name string) Config {
    return &SimpleConfig{
        name:   name,
        v:      viper.New(),
        search: false,
        status: map[time.Time]string{},
    }
}

// Implement methods of Config interface
func (cfg *SimpleConfig) Set(key string, value interface{}) {
    cfg.v.Set(key, value)
}

func (cfg *SimpleConfig) Get(key string) interface{} {
    return cfg.v.Get(key)
}

func (cfg *SimpleConfig) SetDefault(key string, value interface{}) {
    cfg.v.SetDefault(key, value)
}

// Implement other methods similarly

internal/config/config.go

The process involves adding methods to the SimpleConfig struct that delegates their functionality to the corresponding ones of the embedded viper config instance. This way, our SimpleConfig struct serves as a bridge between our application and the spf13/viper library while adhering to the generic Config interface.

func (cfg *SimpleConfig) GetString(key string) string {
    return cfg.v.GetString(key)
}

In essence, Plykan is not just a journey in building a specific application; it's also an exercise in understanding how to develop web applications in Go. As you progress, you'll find that the practices and principles applied here can be valuable in developing other applications. You'll face choices on whether to read configuration values in the main function and pass them to your dependencies, use a more structured approach utilizing the features of the standard library, or opt for a comprehensive solution like spf13/viper. The decision depends on the specific requirements of your project, but this app serves as a practical example to help guide you in making those choices.

Handling Configuration File Provision

When the Load the method is called with a file path as an argument, the Config struct loads the specified configuration file. It sets the cfg.file attribute to the provided file path and disables the search for settings in common config places. This means that it expects the configuration to be explicitly defined in the provided file path, and it will not attempt to search for configuration settings in other locations.

Handling Scenarios Without a Configuration File

If the Load the method is called without providing a file path, the Config struct falls back to default values. It first parses command-line flags using the flag.Parse() function, which checks for two flags:

  • -config-file: This flag allows you to specify the path to the configuration file. If not provided, it defaults to `config.yml".
  • -config-search: This flag determines whether the Config struct should search for settings in common config places such as /etc and $HOME. If config-search is true, the Config struct will search in these common locations. If config-search is false, it will not perform this search.

The Config struct then sets its cfg.file attribute based on the value of the -config-file flag and adjust the cfg.search attribute according to the value of the -config-search flag.

In summary, the Config struct's behavior depends on whether a configuration file path is provided to the Load method and the values of the -config-file and -config-search flags. It can load a specified configuration file, use default values, and search for settings in common config places based on these inputs, providing flexibility in how configuration is managed in your application.

Choice of YAML Format and Configuration Structure

We've opted to use the YAML format. YAML provides a human-readable and structured way to store configuration data, making it easier to manage and maintain. To adhere to standard practices, we've placed our configuration file under the configs/config.yml path within our project:

log:
  level: "debug"

http:
  web:
    server:
      host: localhost
      port: 8080

configs/config.yml

The initial structure of our configuration file is designed to accommodate our application's core settings. Currently, it includes configuration for logging and the HTTP web server.

As our application evolves and additional functionalities are introduced, we'll expand this configuration file to encompass new settings for various components such as databases, API servers, and more.

Dot Notation

One of the convenient features of our Config library is that it allows us to access values from the YAML file using dot notation. For instance, in our Server implementation in internal/web, we can retrieve the server's address like this.

func (srv *Server) Cfg() config.Config {  
	return srv.cfg  
}

func (srv *Server) Address() string {
	host := srv.Cfg().GetString("http.web.server.host")
	port := srv.Cfg().GetInt("http.web.server.port")
	return fmt.Sprintf("%s:%d", host, port)
}

Now, let's update the server's start function

func (srv *Server) Start(ctx context.Context) error {  
	srv.Server = http.Server{  
		Addr: srv.Address(),  
		// ...  
	}
}

main.go

We just have to update main.go to adjust it to our new configuration mechanism.

package main

import (
	"context"
	"github.com/solutioncrafting/plykan/internal/config"
	l "github.com/solutioncrafting/plykan/internal/log"
	"github.com/solutioncrafting/plykan/internal/web"
)

const (
	name     = "plykan"
	logLevel = "debug"
)

var (
	log l.Logger = l.NewLogger(logLevel)
)

func main() {
	cfg, err := config.NewConfig(name).Load()
	if err != nil {
		log.Error(err)
	}

	logLevel := cfg.GetString("log.level")
	log.SetLogLevel(logLevel)

	server := web.NewServer("web-server", cfg, log)
	err = server.Start(context.Background())
	if err != nil {
		log.Error(err)
	}
}

Makefile

To streamline the process of launching Plykan with a specific configuration file, we can create a Makefile. This Makefile will allow us to execute commands and automate tasks with ease. In this case, we'll create a target named run that runs our application while specifying the configuration file.

run:
  go run main.go --config-file=configs/config.yml

Now, by running make run, you can easily launch the application while providing a configuration file to initialize it.

Up Next: App & Models

Before granting our app access to the database, we're going to dive deep into key concepts that will improve the structure and functionality of our application.

In the upcoming chapter, we'll introduce the concept of the App as the root entity of our board. The App will shoulder the vital responsibility of initializing and orchestrating the lifecycle of all Plykan dependencies. This approach ensures that all the service-related logic is well-organized, freeing us from the complexities of constructing the dependency tree directly in our main function. Additionally, it lays the foundation for potential future extraction of functionality into a common library, framework, or code generator.

Finally, our business entities will seamlessly integrate into the Plykan application, finding their place. We will establish a shared foundation of functionality among them, in order to remove repetition from the codebase.

See you!

References