Design Patterns in Go: Abstract Factory, the flexible production of objects

Introduction

The Abstract Factory Pattern is a way to group the creation of related objects, like products of a certain brand or make by building a common factory interface. If this all sounds abstract, a picture can help:

A short breakdown:

  • All the factories have a common interface called AbstractFactory
  • The concrete factories, in our diagram there are two, implement this interface
  • These concrete factories produce objects with a common interface so those are interchangeable. In our example those are called AbstractProductOne and AbstractProductTwo

This is a somewhat more complicated design pattern, but when used well can lead to interchangeable concrete implementations. However, considering the relative complexity of this pattern, it will require extra initial work. Also, due to the high abstraction levels, it can lead in some cases to code that is more difficult to test, debug and maintain.

So, even though this pattern is very flexible and powerful, use it wisely and sparingly.

Implementation in Go

After you create the project, we will start the preliminaries:

import "fmt"

In our example we will define a VehicleFactory interface, to encapsulate two factories, one who produces Brand A vehicles, and one who does the same for Brand B The vehicles can be either cars or bikes.

The cars have an AbstractCar interface, and likewise, the bikes have an AbstractBike interface.

The VehicleFactory interface

We will start by defining the VehicleFactory interface itself:

type VehicleFactory interface {
	CreateCar(color string) AbstractCar
	CreateBike(numberOfWheels int8) AbstractBike
}

In it we see two different methods used to make the two different products i.e. cars and bikes.

The interface for cars and bikes

Next we define the interfaces for both the car and the bike. Both interfaces have a GetDescription() method to render a string representation of themselves:

type AbstractCar interface {
	GetDescription() string
}

type AbstractBike interface {
	GetDescription() string
}

The vehicle structs

Next we come to vehicles themselves, first the BrandACar vehicle struct:

type BrandACar struct {
	make  string
	color string
}

This struct has two fields, both private. In Go this is done by starting the fieldname with a lower-case letter.

The GetDescription() is then quite straightforward:

func (c *BrandACar) GetDescription() string {
	return fmt.Sprintf("Brand A Car - Make: %s, Color: %s", c.make, c.color)
}

This method just returns a string representation of the object.

The BrandABike struct is fashioned in a similar manner:

type BrandABike struct {
	make           string
	numberOfWheels int8
}

func (b *BrandABike) GetDescription() string {
	return fmt.Sprintf("Brand A Bike - Make: %s, Number of Wheels: %d", b.make, b.numberOfWheels)
}

Again, some private fields for the make and the number of wheels a bike has.

The fields in both structs are private because in our setup, once the structs are constructed, there is no need to change the inner state of the struct. Mind you, that is for this particular setup.

Not surprisingly, the BrandBCar and the BrandBBike have similar code:

type BrandBCar struct {
	make  string
	color string
}

func (c *BrandBCar) GetDescription() string {
	return fmt.Sprintf("Brand B Car - Make: %s, Color: %s", c.make, c.color)
}

type BrandBBike struct {
	make           string
	numberOfWheels int8
}

func (b *BrandBBike) GetDescription() string {
	return fmt.Sprintf("Brand B Bike - Make: %s, Number of Wheels: %d", b.make, b.numberOfWheels)
}

The concrete factories

Now we come to the interesting bit, the first concrete factory:

type BrandAFactory struct{}

func (f *BrandAFactory) CreateCar(color string) AbstractCar {
	return &BrandACar{make: "Brand A", color: color}
}
func (f *BrandAFactory) CreateBike(numberOfWheels int8) AbstractBike {
	return &BrandABike{make: "Brand A", numberOfWheels: numberOfWheels}
}

As you can see we have two factory methods, both returning an interface type, so for the client, the underlying object and the underlying implementation are completely transparent. In both methods, for our particular case, we simply initialize the struct and return it.

The BrandBFactory is very similar:

type BrandBFactory struct{}

func (f *BrandBFactory) CreateCar(color string) AbstractCar {
	return &BrandBCar{make: "Brand B", color: color}
}

func (f *BrandBFactory) CreateBike(numberOfWheels int8) AbstractBike {
	return &BrandBBike{make: "Brand B", numberOfWheels: numberOfWheels}
}

Finding the right factory

Finally we implement a utility method to find the right factory:

func getFactory(brand string) (VehicleFactory, error) {
	switch brand {
	case "BrandA":
		return &BrandAFactory{}, nil
	case "BrandB":
		return &BrandBFactory{}, nil
	default:
		return nil, fmt.Errorf("unknown brand: %s", brand)
	}
}

Testing time

Now it is time to test our ideas:

func main() {
	brandAFactory, err := getFactory("BrandA")
	if err != nil {
		fmt.Println(err)
		return
	}
	carA := brandAFactory.CreateCar("Red")
	bikeA := brandAFactory.CreateBike(2)
	fmt.Println(carA.GetDescription())
	fmt.Println(bikeA.GetDescription())

	brandBFactory, err := getFactory("BrandB")
	if err != nil {
		fmt.Println(err)
		return
	}
	carB := brandBFactory.CreateCar("Blue")
	bikeB := brandBFactory.CreateBike(3)
	fmt.Println(carB.GetDescription())
	fmt.Println(bikeB.GetDescription())
}

Line by line:

  1. We declare a variable of type BrandAFactory Remember that VehicleFactory is an interface class, which is implemented by the BrandAFactory.
  2. We next create both a car and a bike, and print out their descriptions.
  3. We do the same for the BrandBFactory type.

Conclusion

As you can see, the Abstract Factory Pattern takes some effort to set up, but it pays off when your project needs flexibility and consistency across families of related objects. By grouping creation logic behind factory interfaces, you can easily switch between brands, products, or entire implementations — even at runtime — without changing your client code.

However, the added abstraction does come at a cost: more boilerplate and potentially harder debugging. So, use it when you truly need interchangeable product families or want to enforce clear creation boundaries. When applied thoughtfully, the Abstract Factory Pattern can make your architecture cleaner, more scalable, and much easier to extend in the long run.

The Code Nomad
The Code Nomad
Articles: 167

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *