Skip to content

Welcome to Tech by Example

Menu
  • Home
  • Posts
  • System Design Questions
Menu

Interview Question: Design an Object Pool

Posted on January 8, 2022January 11, 2022 by admin

Table of Contents

  • Overview
  • When we want to create a object pool
  • UML Diagram
  • Low–Level Design
  • Program
  • Full Working Code:
  • Conclusion

Overview

The Object Pool Design Pattern can be used to design an Object Pool. It is a creational design pattern in which a pool of objects is initialized and created beforehand and kept in a pool. As and when needed, a client can request an object from the pool, use it, and return it to the pool. The object in the pool is never destroyed. In this tutorial, we will look at

  • When we want to create a object pool
  • UML diagram for the design
  • Low-Level Design
  • Full Working Code

When we want to create a object pool

  • When the cost to create the object of the class is high and the number of such objects that will be needed at a particular time is not much.

-Let’s take the example of DB connections. Each of the connection object creation is the cost is high as there is network calls involved and also at a time not more than a certain connection might be needed. The object pool design pattern is perfectly suitable for such cases.

  • When the pool object is the immutable object

         -Again take the example of DB connection again. A DB connection is an immutable object. Almost none of its property needs to be changed

  • For performance reasons. It will boost the application performance significantly since the pool is already created

UML Diagram

Below is the UML diagram of the Object Pool Design

Here is the overall idea

  • We have a Pool Class that does the management of Pool Objects. The Pool class is first initialized with a set of an already created fixed number of pool objects. Then it supports lending, receiving back, and removing of pool objects.
  • There is an interface iPoolOjbect that represents the type of objects that will reside in the Pool. There will be different implementations of this interface depending upon the type of use case. For example, in the case of DB Connections, a DB connection will implement this iPoolObject interface.
  • There is a Client class that uses Pool class to loan a Pool Object. When it is done with the Pool Object, it returns it to the pool.

Low–Level Design

Below is the low-level design expressed in the Go programming language. Later we will see a working example as well

iPoolObject Interface

type iPoolObject interface {
    getID() string
    doSomething()
}

DBConnection Class

type dbconnection struct {
    id string
}

func (c *dbconnection) getID() string 

func (c *dbconnection) doSomething()

Pool Class

type pool struct {
    idle   []iPoolObject
    active []iPoolObject
    capacity int
    mulock   *sync.Mutex
}

//InitPool Initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {}

func (p *pool) loan() (iPoolObject, error) {}

func (p *pool) receive(target iPoolObject) error {}

func (p *pool) remove(target iPoolObject) error {}

func (p *pool) setMaxCapacity(capacity int){}

Client Class

type client struct {
    pool *pool
}

func (c *client) init() {}

func (c *client) doWork() {}    

Program

Here is the full working code if anyone is interested in the Go programming language

iPoolObject.go

package main

type iPoolObject interface {
	getID() string //This is any id which can be used to compare two different pool objects
	doSomething()
}

dbconnection.go

package main

import "fmt"

type dbconnection struct {
	id string
}

func (c *dbconnection) getID() string {
	return c.id
}

func (c *dbconnection) doSomething() {
	fmt.Printf("Connection with id %s in action\n", c.getID())
}

pool.go

package main

import (
	"fmt"
	"sync"
)

type pool struct {
	idle     []iPoolObject
	active   []iPoolObject
	capacity int
	mulock   *sync.Mutex
}

//InitPool Initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {
	if len(poolObjects) == 0 {
		return nil, fmt.Errorf("Cannot craete a pool of 0 length")
	}
	active := make([]iPoolObject, 0)
	pool := &pool{
		idle:     poolObjects,
		active:   active,
		capacity: len(poolObjects),
		mulock:   new(sync.Mutex),
	}
	return pool, nil
}

func (p *pool) loan() (iPoolObject, error) {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	if len(p.idle) == 0 {
		return nil, fmt.Errorf("No pool object free. Please request after sometime")
	}
	obj := p.idle[0]
	p.idle = p.idle[1:]
	p.active = append(p.active, obj)
	fmt.Printf("Loan Pool Object with ID: %s\n", obj.getID())
	return obj, nil
}

func (p *pool) receive(target iPoolObject) error {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	err := p.remove(target)
	if err != nil {
		return err
	}
	p.idle = append(p.idle, target)
	fmt.Printf("Return Pool Object with ID: %s\n", target.getID())
	return nil
}

func (p *pool) remove(target iPoolObject) error {
	currentActiveLength := len(p.active)
	for i, obj := range p.active {
		if obj.getID() == target.getID() {
			p.active[currentActiveLength-1], p.active[i] = p.active[i], p.active[currentActiveLength-1]
			p.active = p.active[:currentActiveLength-1]
			return nil
		}
	}
	return fmt.Errorf("Targe pool object doesn't belong to the pool")
}

func (p *pool) setMaxCapacity(capacity int) {
	p.capacity = capacity
}

client.go

package main

import (
	"fmt"
	"log"
	"strconv"
)

type client struct {
	pool *pool
}

func (c *client) init() {
	connections := make([]iPoolObject, 0)
	for i := 0; i < 3; i++ {
		c := &dbconnection{id: strconv.Itoa(i)}
		connections = append(connections, c)
	}
	var err error
	c.pool, err = initPool(connections)
	if err != nil {
		log.Fatalf("Init Pool Error: %s", err)
	}
}

func (c *client) doWork() {
	fmt.Printf("Capacity: %d\n\n", c.pool.capacity)

	conn1, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn1.doSomething()

	fmt.Printf("InUse: %d\n\n", c.pool.inUse)
	conn2, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn2.doSomething()
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn1)
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn2)
	fmt.Printf("InUse: %d\n", c.pool.inUse)
}

main.go

package main

func main() {
    client := &client{}
    client.init()
    client.doWork()
}

Output

Capacity: 3

Loan Pool Object with ID: 0
Connection with id 0 in action
InUse: 1

Loan Pool Object with ID: 1
Connection with id 1 in action
InUse: 2

Return Pool Object with ID: 0
InUse: 1

Return Pool Object with ID: 1
InUse: 0

Full Working Code:

Here is the full working code in one file

main.go

package main

import (
	"fmt"
	"log"
	"strconv"
	"sync"
)

type pool struct {
	idle     []iPoolObject
	active   []iPoolObject
	capacity int
	inUse    int
	mulock   *sync.Mutex
}

//InitPool Initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {
	if len(poolObjects) == 0 {
		return nil, fmt.Errorf("Cannot craete a pool of 0 length")
	}
	active := make([]iPoolObject, 0)
	pool := &pool{
		idle:     poolObjects,
		active:   active,
		capacity: len(poolObjects),
		mulock:   new(sync.Mutex),
	}
	return pool, nil
}

func (p *pool) loan() (iPoolObject, error) {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	if len(p.idle) == 0 {
		return nil, fmt.Errorf("No pool object free. Please request after sometime")
	}
	obj := p.idle[0]
	p.idle = p.idle[1:]
	p.active = append(p.active, obj)
	p.inUse = p.inUse + 1
	fmt.Printf("Loan Pool Object with ID: %s\n", obj.getID())
	return obj, nil
}

func (p *pool) receive(target iPoolObject) error {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	err := p.remove(target)
	if err != nil {
		return err
	}
	p.idle = append(p.idle, target)
	p.inUse = p.inUse - 1
	fmt.Printf("Return Pool Object with ID: %s\n", target.getID())
	return nil
}

func (p *pool) remove(target iPoolObject) error {
	currentActiveLength := len(p.active)
	for i, obj := range p.active {
		if obj.getID() == target.getID() {
			p.active[currentActiveLength-1], p.active[i] = p.active[i], p.active[currentActiveLength-1]
			p.active = p.active[:currentActiveLength-1]
			return nil
		}
	}
	return fmt.Errorf("Targe pool object doesn't belong to the pool")
}

func (p *pool) setMaxCapacity(capacity int) {
	p.capacity = capacity
}

type iPoolObject interface {
	getID() string //This is any id which can be used to compare two different pool objects
	doSomething()
}

type dbconnection struct {
	id string
}

func (c *dbconnection) getID() string {
	return c.id
}

func (c *dbconnection) doSomething() {
	fmt.Printf("Connection with id %s in action\n", c.getID())
}

type client struct {
	pool *pool
}

func (c *client) init() {
	connections := make([]iPoolObject, 0)
	for i := 0; i < 3; i++ {
		c := &dbconnection{id: strconv.Itoa(i)}
		connections = append(connections, c)
	}
	var err error
	c.pool, err = initPool(connections)
	if err != nil {
		log.Fatalf("Init Pool Error: %s", err)
	}
}

func (c *client) doWork() {
	fmt.Printf("Capacity: %d\n\n", c.pool.capacity)

	conn1, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn1.doSomething()

	fmt.Printf("InUse: %d\n\n", c.pool.inUse)
	conn2, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn2.doSomething()
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn1)
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn2)
	fmt.Printf("InUse: %d\n", c.pool.inUse)
}

func main() {
	client := &client{}
	client.init()
	client.doWork()
}

Output

Capacity: 3

Loan Pool Object with ID: 0
Connection with id 0 in action
InUse: 1

Loan Pool Object with ID: 1
Connection with id 1 in action
InUse: 2

Return Pool Object with ID: 0
InUse: 1

Return Pool Object with ID: 1
InUse: 0

Conclusion

This is all about designing an Object Pool. Hope you have liked this article. Please share feedback in the comments

©2025 Welcome to Tech by Example | Design: Newspaperly WordPress Theme