DEV Community

Palma99
Palma99

Posted on

Feature Flag Service: Experimenting with New Technologies and Architectures

Introduction

I am a junior frontend developer with two years of experience working with Vue.js, I wanted to broaden my knowledge by exploring backend development and experimenting with other frontend frameworks. I decided to create a project from scratch using Go for the backend and Angular 19 for the frontend. The project is a service for managing feature flags, inspired by great existing solutions. My primary focus was on implementing the principles of Clean Architecture while also improving my SQL skills by working directly with PostgreSQL without an ORM.

📚 GitHub repo

The source code can be found here:

Functional overview

The idea was to create a dashboard where users can sign in and manage their projects, environments and flags. Then, project's specific flags can be retrieved by user's app using a public api. The first thing that i wasn't sure about is how to implement this public interaction in a secure way, we will dive into that later, but the idea was to create some sort of library that handle this communication using a public key.

Functional overview of the system

For the dashboard, I aimed to create an intuitive and simple UI for basic operations like creating new projects and flags. My main goal was to clearly display the status of the flags in each environment. This was the initial sketch of the UI.

Dashboard first design idea

I also wanted to implement collaboration, allowing multiple users to access the same project. This necessitates the implementation of a role/permission system, where, for example, only the owner can delete a project.

First step: Designing the database

Let's dive into some technical details, starting with database design. I wanted to use a relational database, but at this stage, I didn’t really care which one to choose. So, as a first step, I started thinking about all the entities and the relationships between them:

  • User: a person that can sign up and have access to the dashboard
  • Project: represent a container of environment and flags
  • Environment: each project can have one or more environments, this is useful for a real world project where multiple test environment can exist.
  • Flag: represent a single feature that can be enabled or not

Database schema

I started creating many-to-many relationship between users and project, so we can easily implement collaboration as mentioned before.
The one-to-many relationship between project and environment it's pretty straight forward as one project can have multiple environments.

Nothing special so far, but things started to get tricky when I encountered the flag table.

The first idea that came to mind was that each flag should be directly related to an environment. This seemed logical, given that we’re working in a multi-environment system where the end user needs to request flags for a specific environment. However, I quickly realized that this could introduce some introduce some complexities.

Imagine you're inside the dashboard for a specific project that has 3 environments (TEST, QA, PROD), and you want to create a new flag called 'dark_mode_experimental'. This means you would need to create 3 new rows in the flag table, one for each environment. Then, if you want to update the flag name to 'dark_mode', you would need to update all the rows accordingly. The same applies if you want to delete a flag. Furthermore, if a project already has some flags and you want to add another environment (let's say 'DEV'), you would need to duplicate all the existing flags for this new environment, which can lead to increased complexity and potential maintenance issues.

The final solution is that flags belong to a project while maintaining a many-to-many relationship with environments through the flag_environment table. This table allows us to store the flag status for each specific environment. By doing so, we can easily manage flag updates and deletions without needing to duplicate or update rows across multiple environments.

What happens when a new environment is created within a project is quite simple: we just add a new row in the environment table. The relations in the flag_environment table are not created immediately but only after a flag is updated. This ensures that we avoid unnecessary entries for environments where no flags have been modified yet. Once a flag is updated, the corresponding relation in the flag_environment table is established, allowing us to track the flag's status in the new environment.

Step two: Implementing Go backend

As I mentioned, I'm more of a frontend person, but I have a strong interest in learning backend technologies. During my studies, I worked with several different languages, mostly C, Java, and Python. These are great languages, but I wanted to try something new. Recently, I heard a lot of positive things about Go, so I decided to give it a shot.

There are a couple of things I want to mention before diving into the code. My main goal here was to structure the code by following clean architecture principles. This approach helps ensure that the code remains modular, maintainable, and testable. Additionally, for handling data, I chose not to use any ORM because I believe that for a study project, writing raw SQL is more instructive and provides a deeper understanding of how data is managed at the database level.

Clean architecture

Let's start with folder structure

Project folders structure

In Go, it's common to have a folder called cmd that contains the entry point of the program, and a folder called internal for all the application code. That's what I did — I created two subfolders inside cmd: one for the API version and one for the CLI version of the service.

cmd folder

There are some great articles about clean architecture, and the structure I implemented is quite standard, so I won't go into detail about what each folder represents. I just want to highlight some parts of the code that demonstrate how the principles are applied and how the different components interact with each other in the project.

Dependency injection

As I mentioned, I'm new to the Go world and still getting familiar with the ecosystem. From what I've seen so far, there aren't any major frameworks like those in the .NET or Java ecosystems that handle dependency injection in such a clean way. Therefore, I decided to implement dependency injection in a very 'vanilla' way, without relying on any external frameworks. Here is an example

// Create a keyService instance 
keyService := services.NewKeyService()

// Create an instance of PgEnvironmentRepository 
//  (which implements EnvironmentRepository interface)
environmentRepository := infrastructure.NewPgEnvironmentRepository(db)

// Create an instance of PgProjectRepository 
//  (which implements ProjectRepository interface)
projectRepository := infrastructure.NewPgProjectRepository(db)

// Create the interactor for the environment, injecting the dependencies
environmentInteractor := usecase.NewEnvironmentInteractor(
    environmentRepository,
    projectRepository,
    keyService,
)
Enter fullscreen mode Exit fullscreen mode

Interactors

An interactor is a struct with some dependencies that has methods to fulfill some business use cases, e.g.

type EnvironmentInteractor struct {
    // These types are interfaces
    environmentRepository repository.EnvironmentRepository
    projectRepository     repository.ProjectRepository
    keyService            *services.KeyService
}

func NewEnvironmentInteractor(
    environmentRepository repository.EnvironmentRepository,
    projectRepository repository.ProjectRepository,
    keyService *services.KeyService,
) *EnvironmentInteractor {
    return &EnvironmentInteractor{
        environmentRepository,
        projectRepository,
        keyService,
    }
}

func (i *EnvironmentInteractor) CreateEnvironment(
    envName string,
    projectId int64,
    userId int
) error {

    if (envName == "") || (projectId == 0) {
        return errors.New("error during creation of environment, name and project id are required")
    }

    pk, err := i.keyService.GeneratePublicKey()
    if err != nil {
        return errors.New("error during creation of environment")
    }

    project, err := i.projectRepository.GetProjectDetails(projectId)
    if err != nil {
        return errors.New("error during creation of environment")
    }

    if !project.UserHasPermission(
        userId, 
        entity.PermissionCreateProjectEnvironment
    ) {
        return errors.New("user does not have permission to create environment in this project")
    }

    environment := &entity.Environment{
        Name:      envName,
        PublicKey: pk,
        ProjectID: projectId,
    }

    if project.EnvironmentWithSameNameAlreadyExists(*environment) {
        return errors.New("environment with same name already exists on this project")
    }

    err = i.environmentRepository.CreateEnvironment(environment)
    if err != nil {
        return errors.New("error during creation of environment")
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

There are several aspects that can be improved, such as error handling or using a factory to create entities. However, the key idea is that the 'Create Environment' use case is managed within this method, which does not rely on any concrete implementation.

Entities

In this project, I implemented anemic entities, which is not ideal. For example, all fields are public so they can be serialized using Go's standard library, although I'm not fully convinced this is the best approach.

package domain

type Environment struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    ProjectID int64  `json:"project_id"`
    PublicKey string `json:"public_key"`
}

type EnvironmentWithFlags struct {
    Environment
    Flags []Flag `json:"flags"`
}
Enter fullscreen mode Exit fullscreen mode

Infrastructure

The infrastructure folder contains all the implementations related to external dependencies. In this case, it includes the repository implementation for PostgreSQL and the middleware used by the HTTP framework.

I decided to use PostgreSQL as the database for no particular reason (well, maybe because I already had a Docker image pulled). However, thanks to clean architecture, it's easy to swap the database by simply implementing a new repository that adheres to the same interface. For example:


type PgFlagRepository struct {
    db *sql.DB
}

func NewPgFlagRepository(db *sql.DB) *PgFlagRepository {
    return &PgFlagRepository{
        db,
    }
}

func (r *PgFlagRepository) UpdateFlagEnvironment(environmentId int, flagsToUpdate []domain.Flag) error {
    tx, err := r.db.Begin()
    if err != nil {
        return err
    }

    defer tx.Rollback()

    for _, flag := range flagsToUpdate {
        if err := upsertFlagEnvironment(tx, environmentId, flag); err != nil {
            return err
        }
    }

    if err := tx.Commit(); err != nil {
        return err
    }

    return nil
}

func upsertFlagEnvironment(tx *sql.Tx, environmentId int, flag domain.Flag) error {
    _, err := tx.Exec(`INSERT INTO flag_environment 
        (flag, environment, enabled)
        VALUES ($1, $2, $3)
        ON CONFLICT (flag, environment) DO UPDATE
        SET enabled = EXCLUDED.enabled;`, 
        flag.ID, environmentId, flag.Enabled)

    return err
}
Enter fullscreen mode Exit fullscreen mode

Interfaces

In this case, interfaces refer to the components that allow external systems to interact with the application. For a web API, this typically consists of a function that, for example, extracts request parameters or body data, creates a DTO, and calls the interactor.

func (environmentController *ApiEnvironmentController) CreateEnvironment(w http.ResponseWriter, r *http.Request) {
    userId := r.Context().Value(context_keys.UserIDKey).(int)

    var dto CreateEnvironmentDTO
    err := json.NewDecoder(r.Body).Decode(&dto)
    if err != nil {
        fmt.Print(err)
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    err = environmentController.environmentInteractor.CreateEnvironment(dto.Name, dto.ProjectId, userId)
    if err != nil {
        fmt.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
        return
    }

    w.WriteHeader(http.StatusCreated)
}
Enter fullscreen mode Exit fullscreen mode

Authentication for the admin section

When dealing with authentication in the real world, it's better to relay on well tested library to improve security. However this for this simple project i decided to implement a username and password authentication that works with JWT tokens from scratch, using this popular library 'github.com/golang-jwt/jwt/v5'.

Implementation of the "public api"

To allow the end user to access flags for a certain environment, I decided to implement a public key pattern. Essentially, there is a public REST API protected by a middleware that checks for a specific header containing a key. If the key is valid, it grants read-only access to all the enabled flags for that environment. Even if the public key gets stolen, the impact is limited. Since it only grants read-only, the key cannot be used to modify any data or access sensitive information.

That's how i implemented the middleware

func CheckPublicKeyAuthMiddleware(keyService *services.KeyService) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            publicKey := r.URL.Query().Get("public_key")

            if publicKey == "" {
                w.WriteHeader(http.StatusUnauthorized)
                w.Write([]byte("public key not found"))
                return
            }

            if !keyService.IsPublicKey(publicKey) {
                w.WriteHeader(http.StatusUnauthorized)
                w.Write([]byte("public key is not valid"))
                return
            }

            ctx := context.WithValue(r.Context(), context_keys.PublicKeyKey, publicKey)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in the route definition

r.Route("/public/v1", func(r chi.Router) {
    r.Use(app_middleware.CheckPublicKeyAuthMiddleware(keyService))
    r.Get("/flags", publicServiceController.GetFlagsByPublicKey)
})
Enter fullscreen mode Exit fullscreen mode

SDK

To interact with the public API I wrote a simple typescript SDK that allow to easily communicate with the service. It provides basic caching and types, and can be used by any javascript app.

import { FeatureFlagService, getFFS } from 'ffs'

FeatureFlagService.init({
  environmentKey: "PK_test_public_key"
})

document.addEventListener("DOMContentLoaded", async () => {
    // Contains an array of active flags
    const activeFlags = await getFFS().getActiveFlags()
});
Enter fullscreen mode Exit fullscreen mode

Admin dashboard in Angular 19

Once I completed the backend, I wanted to interact with my service through a comfortable UI. Angular 19 had just come out with stable signals and other cool features, so I decided to use it for the frontend. I didn’t want to spend too much time designing UI components, hence I decided to use a component library that provides pre-built, customizable components. This allowed me to focus more on the core functionality and user experience. The library i choose is Taiga UI.

The frontend is quite simple, a bit different from the initial sketch but it provides all the features i need.

List of user projects
Ui project list

Project editor
Dashboard project editor

An interesting feature I added consists of a section where you can test the payload of the public api for the environment. It works by making an api call to public endpoint using the environment key of the selected environment, and then it shows the response.

Payload tester

Next steps

  • Unit testing: Clean architecture is great and makes things easier to test, but I haven’t written a single test for my business logic yet. This is definitely something to focus on moving forward.
  • Collaboration is supported, but there is currently no way to "invite" someone to join a project (just manually on db). In the future, it would be cool to implement an invitation system, allowing users to easily add collaborators and manage team access.

Top comments (0)