Universal MongoDB database driver for Go with built-in observability and functional options pattern.
A Go library for connecting to and managing MongoDB with a unified interface using the functional options builder pattern. Includes built-in OpenTelemetry instrumentation for comprehensive observability.
- MongoDB Support: Full MongoDB driver integration with connection pooling
- Options Builder Pattern: Clean, fluent interface for configuration
- Built-in Validation: Compile-time type safety with validation
- OpenTelemetry Integration: Distributed tracing and observability out of the box
- Context Support: Full context.Context support for timeouts and cancellation
- Comprehensive Tests: Full test coverage including mocks
- Production Ready: Optimized for high-performance applications
go get github.com/uug-ai/databasepackage main
import (
"context"
"log"
"time"
"github.com/uug-ai/database/pkg/database"
)
func main() {
// Build MongoDB options
opts := database.NewMongoOptions().
SetUri("mongodb://localhost:27017").
SetHost("localhost").
SetAuthSource("admin").
SetAuthMechanism("SCRAM-SHA-256").
SetReplicaSet("rs0").
SetUsername("user").
SetPassword("password").
SetTimeout(10).
Build()
// Create database client with options
db, err := database.New(opts)
if err != nil {
log.Fatal(err)
}
// Ping the database to verify connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.Ping(ctx); err != nil {
log.Fatal(err)
}
log.Println("Successfully connected to MongoDB!")
}All components use the options builder pattern (similar to MongoDB's official driver). This provides:
- Clean Syntax: Build options separately, then pass to constructor
- Readability: Self-documenting method chains
- Separation of Concerns: Options building is separate from client creation
- Validation: Built-in validation when creating the client
- Type Safety: Compile-time type checking
- Flexibility: Configure only what you need
Each database connection follows this pattern:
- Build Options using
database.NewMongoOptions()with method chaining - Call
.Build()to get the options object - Create Client by passing options to
database.New(opts) - Use the client for database operations
The MongoDB integration demonstrates the options builder pattern:
package main
import (
"context"
"log"
"time"
"github.com/uug-ai/database/pkg/database"
)
func main() {
// Build MongoDB options
opts := database.NewMongoOptions().
SetUri("mongodb+srv://user:password@cluster.mongodb.net/?retryWrites=true&w=majority").
SetHost("cluster.mongodb.net").
SetAuthSource("admin").
SetAuthMechanism("SCRAM-SHA-256").
SetUsername("user").
SetPassword("password").
SetTimeout(30).
SetRetryWrites(true).
Build()
// Create database client with options
db, err := database.New(opts)
if err != nil {
log.Fatal(err)
}
// Perform database operations
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err = db.Ping(ctx)
if err != nil {
log.Fatal(err)
}
log.Println("Connected to MongoDB successfully!")
}Available Methods:
.SetUri(uri string)- MongoDB connection URI.SetHost(host string)- Database host address.SetAuthSource(source string)- Authentication source database.SetAuthMechanism(mechanism string)- Authentication mechanism (e.g., SCRAM-SHA-256).SetReplicaSet(replicaSet string)- Replica set name.SetUsername(username string)- Database username.SetPassword(password string)- Database password.SetTimeout(seconds int)- Connection timeout in seconds.SetRetryWrites(retry bool)- Enable automatic retry writes.Build()- Returns the MongoOptions object
.
├── pkg/
│ └── database/ # Core database implementation
│ ├── database.go # Main Database struct
│ ├── mongodb.go # MongoDB client implementation
│ ├── mongodb_test.go # MongoDB tests
│ └── option.go # Functional option types
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
└── README.md
opts := database.NewMongoOptions().
SetUri("mongodb://localhost:27017").
SetHost("localhost").
SetAuthSource("admin").
SetAuthMechanism("SCRAM-SHA-256").
SetReplicaSet("rs0").
SetUsername("admin").
SetPassword("password").
SetTimeout(10).
Build()
db, err := database.New(opts)You can load configuration from environment variables:
import "os"
opts := database.NewMongoOptions().
SetUri(os.Getenv("MONGO_URI")).
SetHost(os.Getenv("MONGO_HOST")).
SetAuthSource(os.Getenv("MONGO_AUTH_SOURCE")).
SetAuthMechanism(os.Getenv("MONGO_AUTH_MECHANISM")).
SetReplicaSet(os.Getenv("MONGO_REPLICA_SET")).
SetUsername(os.Getenv("MONGO_USERNAME")).
SetPassword(os.Getenv("MONGO_PASSWORD")).
SetTimeout(30).
Build()
db, err := database.New(opts)Example .env file:
# MongoDB Configuration
MONGO_URI=mongodb://localhost:27017
MONGO_HOST=localhost
MONGO_AUTH_SOURCE=admin
MONGO_AUTH_MECHANISM=SCRAM-SHA-256
MONGO_REPLICA_SET=rs0
MONGO_USERNAME=admin
MONGO_PASSWORD=passwordMongoDB options use go-playground/validator for configuration validation. All required fields must be provided:
Uri- Connection URI (required)Host- Database host (required)AuthSource- Auth source database (required)AuthMechanism- Auth mechanism type (required)ReplicaSet- Replica set name (required)Username- Database username (required)Password- Database password (required)Timeout- Connection timeout >= 0 (required)
Validation is automatically performed when calling database.New(opts), ensuring invalid configurations are caught before the client is created.
The options builder pattern provides clear error handling:
// Build options (no validation here)
opts := database.NewMongoOptions().
SetUri("mongodb://localhost:27017").
SetHost("localhost").
// Missing required fields...
Build()
// Validation happens when creating the client
db, err := database.New(opts)
if err != nil {
// Validation error caught at client creation time
log.Printf("Configuration error: %v", err)
return
}
// If we get here, the configuration is valid
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.Ping(ctx)
if err != nil {
// Runtime error during operation
log.Printf("Connection error: %v", err)
return
}Run the test suite:
go test ./...Run tests with coverage:
go test -cover ./...Run tests for specific components:
# Database tests
go test ./pkg/database -v
# MongoDB tests
go test ./pkg/database -run TestMongo
# Mock tests
go test ./pkg/database -run TestMockDatabaseThe package includes a complete mock implementation of the DatabaseInterface that allows you to control the behavior of database operations in your tests without needing a real database connection.
import (
"context"
"testing"
"github.com/uug-ai/database/pkg/database"
)
func TestMyFunction(t *testing.T) {
// Create a new mock database
mock := database.NewMockDatabase()
// Set up expectations for what the mock should return
expectedUser := map[string]any{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
}
mock.ExpectFindOne(expectedUser, nil)
// Inject the mock into your Database instance
opts := database.NewMongoOptions().
SetUri("mongodb://localhost").
SetTimeout(5000).
Build()
db, err := database.New(opts, mock)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
// Use the database - it will use your mock
result, err := db.Client.FindOne(context.Background(), "testdb", "users", map[string]any{"id": 1})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Verify the call was tracked
if len(mock.FindOneCalls) != 1 {
t.Errorf("expected 1 FindOne call, got %d", len(mock.FindOneCalls))
}
}Expect Multiple Results:
mock := database.NewMockDatabase()
// Mock Find to return multiple documents
users := []map[string]any{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
}
mock.ExpectFind(users, nil)
result, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// result contains the mocked usersExpect Errors:
mock := database.NewMockDatabase()
// Mock a connection error
mock.ExpectPing(errors.New("connection failed"))
err := mock.Ping(ctx)
// err will be "connection failed"Custom Behavior:
mock := database.NewMockDatabase()
// Define custom logic based on input
mock.FindFunc = func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
filterMap := filter.(map[string]any)
if filterMap["status"] == "active" {
return []map[string]any{{"id": 1, "status": "active"}}, nil
}
return []map[string]any{}, nil
}Sequential Responses (Queue Pattern):
mock := database.NewMockDatabase()
// Queue multiple responses for sequential calls
// Each call consumes the next item in the queue (FIFO)
users := []map[string]any{{"id": 1, "name": "Alice"}}
notifications := []map[string]any{{"id": 1, "message": "Hello"}}
settings := []map[string]any{{"key": "theme", "value": "dark"}}
mock.QueueFind(users, nil).
QueueFind(notifications, nil).
QueueFind(settings, nil)
// First call returns users
result1, _ := mock.Find(ctx, "testdb", "users", map[string]any{})
// result1 contains users
// Second call returns notifications
result2, _ := mock.Find(ctx, "testdb", "notifications", map[string]any{})
// result2 contains notifications
// Third call returns settings
result3, _ := mock.Find(ctx, "testdb", "settings", map[string]any{})
// result3 contains settings
// Fourth call falls back to default behavior (empty slice)
result4, _ := mock.Find(ctx, "testdb", "other", map[string]any{})
// result4 is []any{}Queue with Errors:
mock := database.NewMockDatabase()
// Queue responses including errors
mock.QueueFind([]map[string]any{{"id": 1}}, nil).
QueueFind(nil, errors.New("connection timeout")).
QueueFind([]map[string]any{{"id": 2}}, nil)
// First call succeeds
result1, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// err is nil, result1 has data
// Second call returns error
result2, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// err is "connection timeout"
// Third call succeeds again
result3, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// err is nil, result3 has dataTrack Call History:
mock := database.NewMockDatabase()
// Make some calls
mock.Find(ctx, "testdb", "users", map[string]any{})
mock.FindOne(ctx, "testdb", "users", map[string]any{"id": 1})
// Verify the calls
if len(mock.FindCalls) != 1 {
t.Error("expected 1 Find call")
}
if mock.FindCalls[0].Collection != "users" {
t.Error("expected collection to be 'users'")
}
// Reset call history for the next test
mock.Reset()The MockDatabase type provides:
Setup Methods:
NewMockDatabase(): Creates a new mock with sensible defaultsExpectPing(err error): Set expected Ping behavior (for all calls)ExpectFind(result any, err error): Set expected Find behavior (for all calls)ExpectFindOne(result any, err error): Set expected FindOne behavior (for all calls)
Sequential Queue Methods:
QueuePing(err error): Add a Ping response to the queue for sequential callsQueueFind(result any, err error): Add a Find response to the queue for sequential callsQueueFindOne(result any, err error): Add a FindOne response to the queue for sequential calls
Custom Function Handlers:
PingFunc: Custom function for Ping behaviorFindFunc: Custom function for Find behaviorFindOneFunc: Custom function for FindOne behavior
Call Tracking:
PingCalls: Slice of all Ping calls madeFindCalls: Slice of all Find calls madeFindOneCalls: Slice of all FindOne calls made
Utility Methods:
Reset(): Clear all call history and queues
Execution Priority:
- Queued responses (consumed FIFO) - highest priority
- Custom function handlers (Func properties)
- Default behavior - fallback
This package includes built-in OpenTelemetry instrumentation for MongoDB operations:
import (
"github.com/uug-ai/database/pkg/database"
"go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo"
)
// OpenTelemetry is automatically configured in NewMongoClient
opts := database.NewMongoOptions().
SetUri("mongodb://localhost:27017").
// ... other options
Build()
db, err := database.New(opts)
// Automatic tracing enabled!Contributions are welcome! When adding new features or database drivers, please follow the options builder pattern demonstrated in this repository.
- Fork the repository
- Create a feature branch (
git checkout -b feat/amazing-feature) - Follow the options builder pattern
- Add comprehensive tests for your changes
- Ensure all tests pass:
go test ./... - Commit your changes following Conventional Commits
- Push to your branch (
git push origin feat/amazing-feature) - Open a Pull Request
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, types
Scopes:
database- Core database functionalitymongo- MongoDB driveroptions- Options builderdocs- Documentation updatestests- Test updates
Examples:
feat(mongo): add connection pooling configuration
fix(database): correct context timeout handling
docs(readme): update MongoDB connection examples
refactor(options): simplify builder interface
test(mongo): add replica set failover tests
This project is licensed under the MIT License - see the LICENSE file for details.
This project uses the following key libraries:
- go-playground/validator - Struct validation
- mongo-driver - Official MongoDB Go driver
- OpenTelemetry - Observability and tracing
See go.mod for the complete list of dependencies.
Build options separately from client creation:
opts := database.NewMongoOptions().
SetUri("mongodb://localhost:27017").
SetHost("localhost").
Build()
db, err := database.New(opts)Options building is completely separate from client creation, following the same pattern as MongoDB's official driver.
Compile-time type checking prevents configuration errors.
Configure only the options you need. Method chaining is optional.
Built-in validation when creating the client ensures configurations are correct before use, catching errors early.
Adding new builder methods doesn't break existing code. Simply add new chainable methods to the options builder.
Self-documenting fluent API makes code easy to read and understand:
// Clear and readable - MongoDB style
opts := database.NewMongoOptions().
SetUri("mongodb+srv://user:password@cluster.mongodb.net").
SetHost("cluster.mongodb.net").
SetAuthSource("admin").
SetAuthMechanism("SCRAM-SHA-256").
SetUsername("user").
SetPassword("password").
SetTimeout(30).
Build()
db, err := database.New(opts)- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: See inline code comments and examples above