Explore the Bridge Pattern in Scala, a structural design pattern that separates abstraction from implementation to keep capability models independent from platform-specific behavior.
Bridge pattern: A structural pattern that separates a stable abstraction from the varying implementation behind it, so both can evolve independently.
In Scala, a bridge is useful when the real variability is not “which subclass do I construct?” but “which platform capability sits behind the same high-level workflow?” You often see this in storage clients, notification systems, rendering backends, cloud providers, and effect-runtime integrations.
If one dimension is business meaning and the other is infrastructure choice, inheritance often tangles them:
EmailReportOnS3EmailReportOnLocalDiskPdfReportOnS3PdfReportOnLocalDiskThat explosion is the bridge warning sign. The better structure is usually:
ReportPublisherObjectStore 1trait ObjectStore {
2 def put(path: String, bytes: Array[Byte]): Either[String, Unit]
3}
4
5trait ReportPublisher {
6 def publish(path: String, bytes: Array[Byte]): Either[String, Unit]
7}
8
9final class DefaultReportPublisher(store: ObjectStore) extends ReportPublisher {
10 override def publish(path: String, bytes: Array[Byte]): Either[String, Unit] =
11 store.put(path, bytes)
12}
The abstraction and implementation vary independently now.
Traits and constructor injection are usually enough. You do not need a large OOP hierarchy unless the domain truly requires it. The bridge can be expressed as:
This is one of the places where Scala’s preference for composition over deep inheritance helps the pattern read naturally.
Ask two questions:
If those change for different reasons, a bridge is often appropriate. For example:
If they do not vary independently, a bridge may be unnecessary abstraction.
A weak bridge pretends implementations are identical when they are not. A strong bridge exposes the stable capability while still respecting real differences such as:
The abstraction should smooth accidental complexity, not erase meaningful operational behavior.
In Scala, bridges frequently become clearer when the capability is effectful rather than exception-driven:
1trait EmailGateway[F[_]] {
2 def send(to: String, subject: String, body: String): F[Unit]
3}
That lets the abstraction stay stable while the implementation behind it might be:
The bridge remains explicit because the dependency is a constructor parameter, not hidden global state.
If there is no meaningful implementation variability, the pattern adds ceremony without solving a real structural problem.
If one backend streams, another blocks, and a third offers only eventual consistency, the abstraction must acknowledge those differences somewhere.
If every caller still needs to know provider-specific quirks, the bridge is not carrying its weight.
Use a bridge when one stable domain-facing API must sit over multiple plausible implementations. Keep the capability interface small, inject it explicitly, and avoid hiding major operational differences that callers still need to reason about.