{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/interface-segregation/",
  "description": "Apply SOLID's Interface Segregation Principle in Go with consumer-defined contracts. Learn why small interfaces and implicit implementation matter.",
  "path": "/go/interface-segregation/",
  "publishedAt": "2025-11-01T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "API",
    "Testing"
  ],
  "textContent": "Object-oriented (OO) patterns get a lot of flak in the Go community, and often for good\nreason.\n\nStill, I've found that principles like [SOLID], despite their OO origin, can be useful\nguides when thinking about design in Go.\n\nRecently, while chatting with a few colleagues new to Go, I noticed that some of them had\nspontaneously rediscovered the Interface Segregation Principle (the \"I\" in SOLID) without\neven realizing it. The benefits were obvious, but without a shared vocabulary, it was harder\nto talk about and generalize the idea.\n\nSo I wanted to revisit ISP in the context of Go and show how [small interfaces], [implicit\nimplementation], and [consumer-defined contracts] make interface segregation feel natural\nand lead to code that's easier to test and maintain.\n\n> Clients should not be forced to depend on methods they do not use.\n>\n> -- Robert C. Martin (SOLID, interface segregation principle)\n\nOr, put simply: your code shouldn't accept anything it doesn't use.\n\nConsider this example:\n\nFileStorage has two methods: Save and Load. Now suppose you write a function that only\nneeds to save data:\n\nThis works, but there are a few problems hiding here.\n\nBackup takes a FileStorage directly, so it only works with that type. If you later want\nto back up to memory, a network location, or an encrypted store, you'll need to rewrite the\nfunction. Because it depends on a concrete type, your tests have to use FileStorage too,\nwhich might involve disk I/O or other side effects you don't want in unit tests. And from\nthe function signature, it's not obvious what part of FileStorage the function actually\nuses.\n\nInstead of depending on a specific type, we can depend on an abstraction. In Go, you can\nachieve that through an interface. So let's define one:\n\nNow Backup can take a Storage instead:\n\nBackup now depends on behavior, not implementation. You can plug in anything that\nsatisfies Storage, something that writes to disk, memory, or even a remote service. And\nFileStorage still works without any change.\n\nYou can also test it with a fake:\n\nThat's a step forward. It fixes the coupling issue and makes the tests free of side effects.\nHowever, there's still one issue: Backup only calls Save, yet the Storage interface\nincludes both Save and Load. If Storage later gains more methods, every fake must grow\ntoo, even if those methods aren't used. That's exactly what the ISP warns against.\n\nThe above interface is too broad. So let's narrow it to match what the function actually\nneeds:\n\nThen update the function:\n\nNow the intent is clear. Backup only depends on Save. A test double can just implement\nthat one method:\n\nThe original FileStorage still works fine:\n\nGo's implicit interface satisfaction makes this less ceremonious. Any type with a Save\nmethod automatically satisfies Saver.\n\nThis pattern reflects a broader Go convention: define small interfaces on the consumer side,\nclose to the code that uses them. The consumer knows what subset of behavior it needs and\ncan define a minimal contract for it. If you define the interface on the producer side\ninstead, every consumer is forced to depend on that definition. A single change to the\nproducer's interface can ripple across your codebase unnecessarily.\n\nFrom Go [code review comments]:\n\n> Go interfaces generally belong in the package that uses values of the interface type, not\n> the package that implements those values. The implementing package should return concrete\n> (usually pointer or struct) types: that way, new methods can be added to implementations\n> without requiring extensive refactoring.\n\nThis isn't a strict rule. The standard library defines producer-side interfaces like\nio.Reader and io.Writer, which is fine because they're stable and general-purpose. But\nfor application code, interfaces usually exist in only two places: production code and\ntests. Keeping them near the consumer reduces coupling between multiple packages and keeps\nthe code easier to evolve.\n\nYou'll see this same idea pop up all the time. Take the AWS SDK, for example. It's tempting\nto define a big S3 client interface and use it everywhere:\n\nDepending on such a large interface couples your code to far more than it uses. Any change\nor addition to this interface can ripple through your code and tests for no good reason.\n\nFor example, if your code uploads files, it only needs the PutObject method:\n\nBut accepting the full S3Client here ties UploadReport to an interface that's too broad.\nA fake must implement all the methods just to satisfy it.\n\nIt's better to define a small, consumer-side interface that captures only the operations you\nneed. This is exactly what the [AWS SDK doc] recommends for testing.\n\n> To support mocking, use Go interfaces instead of concrete service client, paginators, and\n> waiter types, such as s3.Client. This allows your application to use patterns like\n> dependency injection to test your application logic.\n\nSimilar to what we've seen before, you can define a single method interface:\n\nAnd then use it in the function:\n\nThe intent is obvious: this function uploads data and depends only on PutObject. The fake\nfor tests is now tiny:\n\nIf we distill the workflow as a general rule of thumb, it'd look like this:\n\n> Insert a seam between two tightly coupled components by placing a consumer-side interface\n> that exposes only the methods the caller invokes.\n\nFin!\n\n\n\n\n\n[solid]:\n    https://en.wikipedia.org/wiki/SOLID\n\n[small interfaces]:\n    https://go.dev/doc/effective_go#interfaces_and_types::text=Interfaces%20with%20only%20one%20or%20two%20methods%20are%20common%20in%20Go%20code%2C%20and%20are%20usually%20given%20a%20name%20derived%20from%20the%20method%2C%20such%20as%20io.Writer%20for%20something%20that%20implements%20Write\n\n[implicit implementation]:\n    https://go.dev/tour/methods/10\n\n[consumer-defined contracts]:\n    https://go.dev/wiki/CodeReviewComments#interfaces::text=Go%20interfaces%20generally,requiring%20extensive%20refactoring\n\n[code review comments]:\n    https://go.dev/wiki/CodeReviewComments#interfaces::text=Go%20interfaces%20generally,the%20real%20implementation\n\n[aws sdk doc]:\n    https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/unit-testing.html#::text=To%20support%20mocking%2C%20use%20Go%20interfaces%20instead%20of%20concrete%20service%20client%2C%20paginators%2C%20and%20waiter%20types%2C%20such%20as%20s3.Client.%20This%20allows%20your%20application%20to%20use%20patterns%20like%20dependency%20injection%20to%20test%20your%20application%20logic.",
  "title": "Revisiting interface segregation in Go"
}