Skip to content

Latest commit

 

History

History
83 lines (51 loc) · 5.26 KB

File metadata and controls

83 lines (51 loc) · 5.26 KB

Factory — Real-World Applications


1. Notification Service

Problem: An application needs to send alerts through different channels (Email, SMS, Push) depending on user preferences or event severity. The sending logic differs per channel, but the rest of the system only cares that a notification was sent.

How Factory helps: NewNotifier(channel) returns a Notifier interface. The routing logic, retry policy, and logging code work against the interface — they never import or branch on the concrete type.

Special case — fallback chains: In production, a primary channel may be unavailable (email provider down, mobile token expired). The caller can hold a slice of []Notifier returned by multiple factory calls and attempt each in order. Because every notifier satisfies the same interface, the fallback loop requires no type assertions:

for _, n := range notifiers {
    if err := n.Send(msg); err == nil {
        break
    }
}

2. Payment Gateway

Problem: An e-commerce platform supports Stripe, PayPal, and bank transfer. Each has a different SDK, different error types, and different confirmation flows, but the checkout service only needs to call Charge(amount) and Refund(txID).

How Factory helps: A NewPaymentGateway(provider string) factory returns a PaymentGateway interface. The checkout service is decoupled from all provider SDKs — it imports only the factory package.

Special case — per-request factory calls: Unlike the Singleton, the Factory creates a fresh instance on every call. A payment gateway instance typically holds request-scoped credentials (an API key tied to the merchant, a correlation ID). Re-using a single instance across requests would leak one merchant's context into another's transaction. The per-call construction model is the correct one here.


3. Storage Backend

Problem: A service writes files to local disk in development, S3 in staging, and GCS in production. Switching between backends must not require code changes in the upload/download logic.

How Factory helps: NewStorage(backend string) returns a Storage interface with Put, Get, and Delete. The environment variable or config value drives the factory; the rest of the application is blind to which backend is active.

Special case — adding a new backend: This is the clearest demonstration of the Open/Closed Principle in action. Adding Azure Blob Storage means:

  1. Create azureStorage struct, implement Storage.
  2. Add one case "AZURE": line in the factory switch.

No other file changes. No callers need to be updated. This is the property the Factory pattern is specifically designed to preserve.


4. Database Driver

Problem: A data-access layer needs to run against PostgreSQL in production and SQLite in tests. Both expose the same query interface, but their connection setup and DSN format differ.

How Factory helps: NewDB(driver, dsn string) returns a DB interface. Tests pass "sqlite" + an in-memory DSN; production passes "postgres" + a connection string from the environment. The repository layer never sees the difference.

Special case — connection pool ownership: A factory that constructs a database connection must decide who owns the lifecycle. If the factory returns a new connection on every call, callers must close it — and forgetting to do so leaks file descriptors. The idiomatic Go pattern is to construct the pool once (often with a Singleton) and have the factory return a handle into that pool rather than a raw new connection. This is a common interview follow-up: "does your factory create the connection or borrow one?"


5. Logger with Formatters

Problem: A service logs as plain text locally, JSON in staging (for Datadog), and a structured protobuf format in production (for a log pipeline). The logging call sites use a single Log(level, msg, fields) signature.

How Factory helps: NewLogger(format string) returns a Logger interface. Deployment environment drives the format string; the application code is unchanged across environments.

Special case — composing with Singleton: The logger is created once at startup (Singleton) but its formatter is chosen by the Factory. This is a common real-world pairing: the Singleton controls how many instances exist; the Factory controls which variant is constructed. Recognizing when two patterns compose is a common interview follow-up.


Summary: What makes a Factory non-trivial in practice

Application Beyond basic switch + return
Notification Fallback chain across multiple factory-produced instances
Payment Gateway Per-request construction to avoid credential cross-contamination
Storage Backend Open/Closed: new backends added without touching callers
Database Driver Lifecycle ownership — factory vs. pool vs. connection handle
Logger Factory composing with Singleton for single-instance variant selection

The interview insight: the Factory pattern is easy to implement for stateless types with no setup cost. It becomes interesting when instances carry request-scoped state, when construction is expensive (and should be pooled), or when it composes with other patterns like Singleton or Decorator to separate how many from which kind.