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!")
}The FindOne method returns a SingleResultInterface that provides a fluent API for decoding results:
// Define your struct
type User struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Email string `bson:"email"`
}
// Decode directly into a struct using .Into()
var user User
err := db.Client.FindOne(ctx, "mydb", "users", map[string]any{"name": "Alice"}).Into(&user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found user: %s (%s)\n", user.Name, user.Email)Get raw result:
// Get the raw result as any (primitive.D) using .Raw()
result, err := db.Client.FindOne(ctx, "mydb", "users", map[string]any{"name": "Alice"}).Raw()
if err != nil {
log.Fatal(err)
}Check for errors before decoding:
// Check if there was an error (e.g., no document found)
singleResult := db.Client.FindOne(ctx, "mydb", "users", map[string]any{"name": "Unknown"})
if singleResult.Err() != nil {
log.Printf("Query error: %v", singleResult.Err())
return
}
var user User
err := singleResult.Into(&user)// Find returns []any
results, err := db.Client.Find(ctx, "mydb", "users", map[string]any{"status": "active"})
if err != nil {
log.Fatal(err)
}
for _, doc := range results.([]any) {
fmt.Println(doc)
}// Insert one document
id, err := db.Client.InsertOne(ctx, "mydb", "users", map[string]any{
"name": "Bob",
"email": "bob@example.com",
})
// Insert multiple documents
ids, err := db.Client.InsertMany(ctx, "mydb", "users", []any{
map[string]any{"name": "Charlie"},
map[string]any{"name": "Diana"},
})// Delete a single document
deleteOneResult, err := db.Client.DeleteOne(ctx, "mydb", "users", map[string]any{"email": "bob@example.com"})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Deleted %d document\n", deleteOneResult.DeletedCount())
// Delete multiple documents
deleteManyResult, err := db.Client.DeleteMany(ctx, "mydb", "users", map[string]any{"inactive": true})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Deleted %d documents\n", deleteManyResult.DeletedCount())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 fluent API to decode into a struct
var user struct {
ID int `bson:"id"`
Name string `bson:"name"`
Email string `bson:"email"`
}
err = db.Client.FindOne(context.Background(), "testdb", "users", map[string]any{"id": 1}).Into(&user)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("expected name 'Alice', got '%s'", user.Name)
}
// Or get the raw result
result, err := db.Client.FindOne(context.Background(), "testdb", "users", map[string]any{"id": 1}).Raw()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Verify the calls were tracked
if len(mock.FindOneCalls) != 2 {
t.Errorf("expected 2 FindOne calls, 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}).Raw()
// 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) - returns aSingleResultInterfaceExpectDeleteOne(deletedCount int64, err error): Set expected DeleteOne behaviorExpectDeleteMany(deletedCount int64, err error): Set expected DeleteMany behavior
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 callsQueueDeleteOne(deletedCount int64, err error): Add a DeleteOne response to the queue for sequential callsQueueDeleteMany(deletedCount int64, err error): Add a DeleteMany 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 (should returnSingleResultInterface)DeleteOneFunc: Custom function for DeleteOne behaviorDeleteManyFunc: Custom function for DeleteMany behavior
Call Tracking:
PingCalls: Slice of all Ping calls madeFindCalls: Slice of all Find calls madeFindOneCalls: Slice of all FindOne calls madeDeleteOneCalls: Slice of all DeleteOne calls madeDeleteManyCalls: Slice of all DeleteMany calls made
Utility Methods:
Reset(): Clear all call history and queues
SingleResultInterface Methods:
.Into(dest any) error: Decode the result directly into a struct pointer.Raw() (any, error): Get the raw result asany.Err() error: Get any error from the query
Execution Priority:
- Queued responses (consumed FIFO) - highest priority
- Custom function handlers (Func properties)
- Default behavior - fallback
This repository keeps thin GitHub Actions wrapper workflows in .github/workflows and reuses the shared workflow implementations from uug-ai/workflows.
Current wrappers:
pr-build.yml- pull request build and Go test pipelinetest-coverage.yaml- test coverage and Codecov uploadsecurity-scan.yml- container vulnerability scanning with Trivypr-description.yml- automatic pull request description generationissue-userstory-create.yml- manual user story issue creationrelease-create.yml- release image build and manifest publishing
The wrapper files in this repository only define repository-specific triggers and inputs. The shared repository owns the reusable job logic.
The release workflow calls the shared release-create.yml workflow with create_gitops_pr: false.
That means release automation for this repository:
- builds and publishes the container images
- creates the multi-architecture manifests
- does not open a GitOps pull request
These workflows use secrets: inherit, so the repository must provide the secrets expected by the shared workflows, including where relevant:
USERNAMETOKENCODECOV_TOKENAZURE_OPENAI_API_KEY
Azure OpenAI endpoint and version values can be supplied through repository variables used by the shared workflows.
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)