Code Refactoring: Eliminating Duplication with Function Injection in Go

In modern backend systems, it’s common to support multiple entry points for similar business logic, such as searching hotels by city versus by geographic coordinates. At first glance, these features seem distinct. But under the hood, they often share nearly identical processing pipelines.

In our hotel search service, we faced exactly this challenge: two methods, ProcessGeoSearch and ProcessSearch, that were 95% identical, differing only in how they resolved city context and whether they cached pricing data. This duplication made bug fixes risky and feature development slow.

Let’s walk through how we refactored this using function injection—a clean, idiomatic Go pattern that eliminates redundancy while preserving flexibility.

The Problem: Two Almost-Identical Functions

Here’s a simplified view of the original structure:

go
func (s *Service) LocationBasedSearch(ctx context.Context, req QueryParams, data Data) (*Result,
error) {
    //...150+ lines of logic...
    city := data.City
    //...later...
    s.applyRating(ctx, data)
    //...and...
    s.cachePrice(ctx, data)
}
func (s *Service) ProcessSearch(ctx context.Context, req QueryParams, data Data) (*Result, error) {
    //...same 150+ lines...
    // uses shared `city` from param
    s.applyRatingVersionTwo(ctx, data)
    s.cachePriceByOtherParam(ctx, data) // ← This one DOES need caching
}

The differences were subtle but critical:

  • City resolution: per-hotel vs. shared.
  • Rating logic: different method calls.
  • Caching: needed in city search, not in geo search.

Maintaining two copies meant every change had to be applied twice — error-prone and tedious.

The Solution: Extract a Core Function with Injected Behavior

We created a single, reusable core function that accepts behavioral differences as function parameters:

go
func (s *Service) processCore(
ctx context.Context,
req QueryParams, data Data,
cityResolver func(data Data) *hotel.City,
ratingSetter func(context.Context, data Data) error,
cacheFn func(context.Context,data Data),) (*Result, error) {
    // Shared logic // Use injected city resolver
    city := cityResolver(&h) // Apply ratings via injected
    function
    ratingSetter(ctx, hotelIds64, hotelsMap) // Apply pricing
    s.applyPricing(ctx, req, roomsMap, hotelsMap) // Conditionally cache — only if cacheFn does
    something
    cacheFn(ctx, roomsMap, req)
    return result, nil
}

This core function now handles everything — filtering, concurrency, availability aggregation, pricing, and sorting — while delegating only the variable parts to injected functions.

Why This Works So Well in Go

Go’s support for first-class functions makes this pattern natural:

  • No interfaces or inheritance needed.
  • Zero runtime overhead.
  • Full type safety.
  • Easy to test (pass mocks or no-ops).

We also preserved all concurrency logic (goroutines for availability and ratings) inside the core, ensuring thread safety while keeping parallelism intact.

The Result

✅ Zero duplication — one source of truth for search logic.
✅ Clear separation of concerns — core vs. context-specific behavior.
✅ Easy to extend — adding a new search type (e.g., “landmark search”) takes minutes.
✅ Safer changes — fix a bug once, deploy everywhere.

Design Pattern

The design pattern you’ve used is most accurately described as Behavior Parameterization (also known as Strategy Pattern in object-oriented contexts), implemented in Go using first-class functions.

🔍 Breakdown:

1. Behavior Parameterization

  • Definition: Passing what to do (behavior) as a parameter to a method, rather than hardcoding it.
  • In your refactored code, you extracted variable logic (city resolution, rating loading, caching) into function parameters:
go
cityResolver func(data Data),ratingSetter func(context.Context, data Data) error,cacheFn
func(context.Context, data Data),
  • This allows the core algorithm (processSearchCore) to stay generic while delegating decisions to the caller.
✅ This is a functional programming idiom, very idiomatic in Go.

2. Strategy Pattern (OO Equivalent)

In traditional OOP, you’d define an interface like:

go
type CityResolver interface {
    Resolve(data Data)
}
  • But in Go — especially for simple, stateless behaviors — it’s more concise and common to pass functions directly instead of defining interfaces.
  • So your approach is the functional-style Strategy Pattern.
📌 Go proverb: “Functions are values.” → Use them to inject behavior.

Conclusion

The refactoring we performed — replacing two nearly identical search methods with a single, behavior-parameterized core function — is a powerful demonstration of how simple, idiomatic Go patterns can dramatically improve code quality. By injecting variability through functions, we eliminated duplication, reduced the risk of inconsistencies, and made the system easier to test, extend, and maintain.

While this approach leverages the Strategy pattern in a functional style, it’s worth noting that other patterns like Builder can play a complementary role — especially when dealing with complex input configuration. Together, these patterns help us build systems that are not only correct today but also adaptable to tomorrow’s requirements.

In Go, elegance often lies in simplicity: a well-placed function parameter can replace layers of abstraction. By choosing the right pattern for the right problem — behavior customization vs. object construction — we write code that is both pragmatic and principled. And in a fast-moving codebase, that’s the difference between technical debt and technical leverage.

A message from our Founder

Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.

Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️

If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.

And before you go, don’t forget to clap and follow the writer️!