Skip to content

v3: proposal re make tarantool.Future to be interface type #470

@bigbes

Description

@bigbes

Overview

We are considering a significant design change for the upcoming v3 release of the Tarantool Go client: replacing the current concrete Future struct with an interface. This change aims to improve testability, enable better abstraction, and support more flexible usage patterns—while carefully preserving performance.

This issue is a great opportunity for interns to understand Go interface design, API evolution, and trade-offs between abstraction and performance.

Current State

Right now, tarantool.Future is defined as a concrete struct with internal fields and public methods:

type Future struct {
    // unexported fields
}

It exposes the following public methods:

// NewFuture creates a new empty Future for a given Request.
func NewFuture(req Request) *Future

// WaitChan returns a channel that closes when the response arrives or an error occurs.
func (fut *Future) WaitChan() <-chan struct{}

// GetTyped waits for the Future and decodes the response directly into 'result' using msgpack.
// This avoids intermediate []interface{} allocation and is faster than Get().
func (fut *Future) GetTyped(result interface{}) error

// Get waits for the Future and returns data as []interface{} + error.
// Slower than GetTyped due to generic decoding.
func (fut *Future) Get() ([]interface{}, error)

// GetResponse returns the raw Response and error.
func (fut *Future) GetResponse() (Response, error)

// These methods are used internally by the client to fulfill the Future:
func (fut *Future) SetError(err error)
func (fut *Future) SetResponse(header Header, body io.Reader) error

Problem Statement

  1. Hard to mock in tests: Because Future is a concrete type with internal state and private fields, it’s difficult to create lightweight mocks or stubs for unit testing code that depends on it.
  2. Leaky abstraction: Public setters like SetError and SetResponse expose internal mechanics that users shouldn’t need to interact with—these are really meant for internal use by the connection logic.
  3. Tight coupling: Code that consumes a *Future is tightly coupled to this specific implementation, limiting flexibility (e.g., you can’t easily substitute a fake or instrumented future).

Proposed Change (for v3)

Refactor Future into an interface that captures only the observable behavior users need:

type Future interface {
	WaitChan() <-chan struct{}
	Get() ([]interface{}, error)
	GetTyped(result interface{}) error
	GetResponse() (Response, error)
}
  • Remove SetError and SetResponse from the public API (they become internal implementation details).
  • Keep the existing struct (possibly renamed to future or defaultFuture) as the default implementation of this interface.
  • Update NewFuture (or introduce a new factory) to return the interface:
    func NewFuture(req Request) Future

Benefits

  • Testability: Users (and our own tests) can easily create mock Future implementations that return predefined responses or simulate errors—without spinning up a real connection or dealing with channels/mutexes.
  • Cleaner API: Hides internal mutation methods (Set*) that were never meant for public use.
  • Extensibility: Enables alternative Future implementations (e.g., for observability, caching, or async backends) without breaking user code.

Concerns & Considerations

  • Performance: Adding an interface indirection might introduce a tiny overhead. However:
    • Modern Go compilers optimize interface calls well.
    • The cost is likely negligible compared to network I/O and msgpack decoding.
    • We should benchmark before/after (interns: this is a great task!).
  • Naming: Should the interface be called Future, and the concrete type future (unexported)?
  • Go way: It may be agains go way "call by interfaces, return structs".

Tasks

  • Draft the new Future interface.
  • Refactor the existing struct into an unexported implementation.
  • Update all internal usage to work with the interface.
  • Write unit tests that mock the Future interface.
  • Benchmark Get(), GetTyped(), and WaitChan() before/after to verify no significant regression (pprof, go test -cpuprofile, -memprofile, go benches).
  • Update documentation and examples.

Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions