Golang - applying Builder pattern in CRM development.

Golang - applying Builder pattern in CRM development.

When we design API modules, one question may arise: how will the logic of working with optional parameters of our struct be organized? Effectively solving this problem can significantly increase the usability of our API. In this article I will give a concrete example from my practice in CRM development and how the Builder pattern helped me in this issue.

Let's consider one of the modules in CRM. We have Invoices, one of the key functionalities of our system, which has certain business logic. The module accepts various fields - amount, assigned user, array of line items, etc.

 

type Invoice struct {

 ID         string

 Subject string

 AssignedUser int

 CreateTime time.Time

 UpdatedTime time.Time

 LineItems  []item.LineItem

 Total      float64

}

 

func NewInvoice(id string, subject string, user int, lineItems []item.LineItem) (*Invoice, error) {

    // ...

}

 

The developers started using our NewInvoice function and everyone seemed to be happy. But the moment came when clients asked to add new parameters to the module structure - description, discount amount, account ID. And the last field should be mandatory. However, we realize that adding new parameters to the function will break backward compatibility, other modules will be changed. That is, changing one parameter in the function will lead to a forced reworking of other parts of the system.

At the same time, we received a task that the ID field should be mandatory only if Total is not empty.

And the AssignedUser field should have its own logic:

  • If the user is not filled in, we use the default user.
  • If a negative value is passed, throw a validation error.
  • If the value is 0, we use a randomly selected user.
  • In all other cases, we fill in the user passed to us by the client.

How can we make a developer friendly API? Let's look in the direction of the Builder pattern.

The Builder pattern was described in the Gang of Four book and it provided a flexible solution in creating objects. The process of creating an account is isolated from the structure itself. It requires a separate struct, the InvoiceBuilder, which gets methods to configure and create the Invoice.

Let's see a concrete example of how it helps us to create a friendly API that contains all the requirements including managing the assigned user:

 

type Invoice struct {

 ID         *string

 Subject string

 AssignedUser *int

 CreateTime time.Time

 UpdatedTime time.Time

 LineItems  []item.LineItem

 Total      float64

}

 

type InvoiceBuilder struct {

 Invoice

}

 

func NewInvoiceBuilder() *InvoiceBuilder {

 return &InvoiceBuilder{

  Invoice: Invoice{

   CreateTime: time.Now(),

   UpdatedTime  : time.Now(),

  },

 }

}

 

func (b *InvoiceBuilder) WithID(id string) *InvoiceBuilder {

 b.ID = &id

 return b

}

 

func (b *InvoiceBuilder) WithAssignedUser(id int) *InvoiceBuilder {

 b.ID = &id

 return b

}

 

func (b *InvoiceBuilder) WithLineItems(items []item.LineItem) *InvoiceBuilder {

 b.LineItems = &items

 b.Total = float64(len(items)) * 10.0 // Assume each item costs 10.0

 return b

}

 

func (b *InvoiceBuilder) Build() (*Invoice, error) {

 if b.ID == nil && b.Total < 1 {

  return nil, fmt.Errorf("ID can't be empty when amount is 0")

 }

 if len(b.LineItems) == 0 {

  return nil, fmt.Errorf("at least one line item should be there")

 }

 if b.AssignedUser == nil {

    b.AssignedUser = GetDefaultUser()

 } else {

    if *b.AssignedUser == 0 {

        b.AssignedUser = RandomUser()

    } else if *b.AssignedUser < 0 {

        return &Invoice{}, ValidationError("Assigned user is not correct")

    }

 }

 return &b.Invoice, nil

}

 

The InvoiceBuilder struct contains the Invoice configuration. We wrote the WithAssignedUser method, to configure the associated user in the Invoice. Typically, this kind of method configuration returns the Builder itself, so that we can use the method chain (e.g., builder.Foo("foo").Bar("bar")). The key is usually the Build method itself, which contains the initialization logic for the bound user and returns the created account.

There is no single possible implementation of the Builder pattern.

For example, some may prefer an approach where the logic for determining the final value of the user resides in the WithAssignedUser method rather than in the Build method. The purpose of this article is to provide an overview of the Builder pattern, not to look at all possible variations.

 

We can use this pattern in the following way:

 

func main() {

 builder := NewInvoiceBuilder()

 invoice, err := builder.WithID("1").WithLineItems([]item.LineItem{...}).Build()

 

 if err != nil {

  fmt.Println("Failed to build invoice:", err)

  return

 }

 fmt.Println(invoice)

}

 

The first thing we have to do is to create an InvoiceBuilder and use it later to assign optional fields such as ID, products, user. Then, it calls the Build method which performs validation and error checking. If all is well, we return the finished account structure.

This approach makes managing the fields of the structure much more convenient. We don't have to pass a user reference or identifier, these methods accept a string or integer. We also don't have to pass an amount, Builder will calculate it automatically. That said, we can also continue to use the old NewInvoice method if other developers want to use the default configuration.

However, there are certain disadvantages to this approach, in particular, there is the issue of error handling. In other programming languages where an exception is thrown, Builder methods such as WithLineItems can throw an exception if the value is incorrect. If we want to still chain methods when calling Builder, the function cannot return an error. Thus, we need to move validation issues to the Build method. If the client can pass multiple parameters, but we only want to handle the ID field, it makes the error handling issue more complicated.

In this article, we looked at an example of applying the Builder pattern to the Invoice struct in a CRM system. The pattern allows you to conveniently work with optional parameters and create objects with different configurations, making the API more clear and readable for developers.

Benefits of using the Builder pattern:

  • Simplifying object configurations: Separating the object creation process from the struct makes code cleaner and easier to understand, especially when objects have many optional parameters.
  • Ensuring backward compatibility: Adding new parameters to an object's structure does not break backward compatibility, since new parameters can be added via new methods in Builder without affecting existing calls.
  • Readable code: Builder method chaining makes API code more readable, making it easy to keep track of what parameters have been set.
  • Centralized error handling: The Build method allows parameter validation and error handling to be done in one place, making it easier to manage errors.

Disadvantages:

  • Error handling: Moving parameter validation to the Build method can complicate error handling and make it difficult to provide accurate information about the cause of an error.
  • Possible encapsulation violation: Depending on the implementation, some parameters may be accessed externally through Builder, which may violate data encapsulation.

Overall, the Builder pattern provides a powerful tool for creating a friendly and user-friendly API with support for optional parameters. It allows you to isolate the process of creating objects from their structure and simplifies working with different object configurations. However, it is important to use this pattern wisely, taking into account the peculiarities of a particular project and possible limitations.

 

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235