In this post I discuss the benefits of using library wrappers. Part 2 discusses testing and linting of the same. The examples will use Ruby, but the ideas are more broadly applicable.
When I wanted to update the AWS gem used by a large Ruby codebase, I found that there were many breaking changes in the target version. Worse, usage was scattered among hundreds of files, and varied from case to case. There were few easy find-and-replace fixes; instead, usage would have to be edited by hand to conform to the new APIs.
Encapsulation Increases Maintainability
This gem update would have been much easier if it were strictly accessed through a wrapper. By wrapper, I mean: a class that encapsulates another by providing selective access to its methods.
For example, instead of using a class Foo directly, we can wrap it in MyFoo. The implementation of MyFoo is mostly a matter of piping calls into the relevant methods of Foo.
class Foo
def greeting(name)
"Hello, #{name}!"
end
def bar
:bar
end
end
class MyFoo
def initialize
@foo = Foo.new
end
def greeting(name)
@foo.greeting(name)
end
# #bar omitted in the wrapper
end
Turning to the aws-sdk-ruby gem, let’s apply this approach to listing S3 buckets. Here’s an example taken from the gem’s documentation:
# abridged, via https://docs.aws.amazon.com/sdk-for-ruby/v3/api/
s3 = Aws::S3::Client.new
s3.list_buckets.buckets.map(&:name)
Yet, its developer guide (in contrast to the gem docs) implicitly recommends making a wrapper:
# abridged, via https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/hello.html
class BucketListWrapper
attr_reader :s3_resource
def initialize(s3_resource)
@s3_resource = s3_resource
end
def list_buckets
@s3_resource.buckets.map(&:name)
end
end
# usage:
wrapper = BucketListWrapper.new(Aws::S3::Resource.new)
wrapper.list_buckets
The wrapper has several advantages in terms of maintainability. Should any of these APIs change in v4 of the gem, we have a single class to update, and likewise a single contract with callers. By contrast, the unwrapped usage is exposing all methods on the list_buckets response type, and over time callers will inevitably use others besides name, all of which are subject to change. In other words, the wrapper narrows the contract with callers.
Notice also the flexibility provided by the gem: in the first example we list buckets from an Aws::S3::Client, in the second, from an Aws::S3::Resource. If usage were instead fully wrapped, callers would choose neither. Later, if, for example, Aws::S3::Resource were discontinued, we could update only the wrapper and leave callers as they were.
A deeper BucketWrapper class that instead wraps many bucket methods might never grow unwieldy.
Consolidated Configuration Is Easily Changed
Wrappers can also consolidate configuration handling. The AWS SDK can be configured variously using an ambient IAM role, environment variables, SSM, etc. If we initialize the SDK in a single wrapper, we can easily revise our configuration management without updating our callers.
The BucketListWrapper example is incompletely encapsulated because it’s constructed with an Aws::S3::Resource, which puts the burden of configuration on callers.
Gathering Cross-cutting Concerns
Cross-cutting concerns are those that span application boundaries. Logging, monitoring, and error reporting are some common examples. Wrappers are a way to maximize the benefits of these tools. For example, we can ensure that all SDK usage is logged consistently by decorating the wrapper’s methods with a logger. This has the added benefit of de-duplicating logging code within the callers. Note though that, as of January 2025, the AWS Ruby SDK has built-in observability features that might be preferable to rolling your own.
Testing
Wrappers can make it easier to mock external services in tests, but the AWS gem isn’t a good example of this, because it has excellent built-in testing facilities that will be discussed in a followup to this post. Generally speaking, however, wrappers can help to reduce the amount of mocking needed to prevent remote calls/dependencies in tests. For example, if we must always initialize a client with some configuration, then a resource, then call methods on a result, we have a lot more to mock than a single call that encapsulates all of that, like Wrapper.list_buckets.
Closing Thoughts
As always, there are trade-offs to using wrappers. There’s an upfront cost in building out the wrapper, but these are often justified by long-term maintainability gains. Likewise, it can be efficient to leverage the conveniences of an SDK, but again, this can impact long-term maintainability.
We just looked at the pros and cons of encapsulating libraries in wrappers. Some of these practices are more easily accomplished in a greenfield. What if you inherit a codebase with lots of unwrapped usage, and want to implement a wrapper? For that, units tests are necessary. I discuss that in the next post.