Modern applications often rely on multiple datastores within a single microservice to handle diverse use cases. Managing these datastores effectively—while keeping the application lightweight, maintainable, and scalable—requires adopting robust engineering patterns. In this article, we’ll explore how patterns like the Factory Pattern and Dependency Injection (DI) help streamline datasource integration, with practical examples using GoFr, a framework I’ve been contributing to and am familiar with.
Factory Pattern in Action
The Factory Pattern encapsulates initialization logic into reusable functions, making your code clean, modular, and easy to maintain. Here's an example of how you use this pattern in GoFr:
Example with the Factory Pattern
func main() {
app.AddMongo(mongo.New(mongo.Config{
URI: "mongodb://localhost:27017",
Database: "test",
ConnectionTimeout: 4 * time.Second,
}))
}
Factory Implementation (Encapsulated Logic)
// mongo package
func New(config mongo.Config) *mongo.Client {
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(config.URI))
if err != nil {
log.Fatal("Failed to connect to MongoDB:", err)
}
// Additional setup like pinging or timeout logic.
err = client.Ping(context.TODO(), nil)
if err != nil {
log.Fatal("Failed to ping MongoDB:", err)
}
return client
}
Note: This code is for demonstration purposes only. The actual implementation in the GoFr framework may differ from the example provided below. If you're interested in exploring the official logic, feel free to check out their open-source code here.
Key Benefits of the Factory Pattern
1. Encapsulation: The factory is like a prebuilt kitchen—you don't need to worry about plumbing or appliances.
- With a factory: Setup happens in one place; the app just uses it.
- Without a factory:: Setup logic is duplicated everywhere, leading to maintenance nightmares.
2. Reusability: A factory acts as a blueprint.
-
With a factory: Reuse the logic anywhere by calling
mongo.New(config)
. - Without a factory: Setup code is manually duplicated across the app.
3. Centralized Updates: One change in the factory updates all instances automatically.
- With a factory: Update connection logic in one place (e.g., add tracing).
- Without a factory: Hunt for every instance of the logic, increasing the risk of missing some.
4. Testability: Factories make mocking seamless.
- With a factory: Return a mock object during testing to avoid real database connections.
- Without a factory: Tests require spinning up real instances or rewriting mocks everywhere.
Dependency Injection
Dependency Injection (DI) is like hiring a contractor to build a house—you delegate the management of components (e.g., windows, doors) so you can focus on the overall design. In DI, instead of hardcoding dependencies like databases or services, they are passed from the outside, making your application modular, testable, and maintainable.
Examples: Injecting MongoDB as a datasource in GoFr
GoFr streamlines Dependency Injection (DI) through intuitive helper methods. For instance, injecting a MongoDB datasource is as simple as follows:
// AddMongo sets MongoDB in the app's container.
func (a *App) AddMongo(db container.MongoProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-mongo")
db.UseTracer(tracer)
db.Connect()
a.container.Mongo = db
}
Explore the open-source implementation
This approach ensures:
- Minimalism: Lightweight and explicit setup without unnecessary abstractions.
- Observability: Automatic configuration of logging, metrics, and tracing.
- Testability: Easily replace real dependencies with mocks for testing.
Comparing GoFr DI with Other Frameworks
Feature | GoFr | Other DI Frameworks |
---|---|---|
Configuration Encapsulation | Uses helper methods (e.g., AddMongo ) to inject dependencies, reducing boilerplate. |
May rely on automatic dependency resolution, increasing complexity. |
Integrated Observability | Automatically configures logging, metrics, and tracing during dependency injection. | Requires additional setup or external libraries. |
Container-Based Access | Dependencies are stored in a container, accessible throughout the app. | Relies on containers but may use reflection or hidden complexity. |
Explicit vs. Magic | Developers configure dependencies explicitly, providing clarity. | Often relies on annotations, reflection, or automatic wiring. |
Flexibility | Full control over dependency injection, allowing custom configurations. | More abstracted setups that may reduce control. |
Here GoFr, offers greater control and simplicity compared to other DI frameworks. Similarly, you can build clean, lightweight, and testable way to manage dependencies while integrating observability tools seamlessly. AND....That wraps up this article—feel free to share your thoughts in the comments below and spread the word with your peers!
Contribute and Learn
If you're eager to explore Dependency Injection and the Factory Pattern, you can start with these open source issues to tame your Golang skills :
- Support for CockroachDB Datasource in GoFr #1346
- Add MSSQL Datasource Support #984
- Azure Blob Storage as a FileSytem #506
- Google Cloud Storage as a FileSystem #504
Want a Demo? Don't Miss out ! Watch Now ! 🚀
If you're interested in leveraging the datasources provided by GoFr as externalized support using the Factory Pattern and Dependency Injection, check out my latest video!, where I dive into this plug and play approach, demonstrate the implementation, and share a fun, hands-on demo.
Top comments (0)