When we started building the blog pipeline for Kern, we knew we needed to deploy generated content somewhere. Vercel was the obvious target — we already used it for client sites. The straightforward approach would have been to call the Vercel API directly from the pipeline and move on.
Instead, we built an abstract base class called DeploymentTarget with a single concrete implementation for Vercel. This is the kind of thing that looks like overengineering on day one and looks obvious six months later.
Here's why we did it, what the abstraction looks like, and the one condition under which premature abstraction is worth doing.
The Abstraction
It's minimal by design:
class DeploymentTarget(ABC):
def __init__(self, project_id: str, config: dict | None = None):
self.project_id = project_id
self.config = config or {}
@abstractmethod
async def deploy(self, site: dict) -> str:
"""Deploy site content. Returns the live site URL."""
...
@abstractmethod
async def redeploy(self) -> str:
"""Redeploy the last deployed version. Returns the live site URL."""
...
def get_deployment_url(self) -> str | None:
return self.config.get("deployment_url")
Two methods. A factory to resolve the right implementation:
def get_deployment(project_id, db_session=None, config=None):
resolved_config = config or {}
if db_session is not None:
# Read deployment config from the project record
...
target_type = resolved_config.get("deployment_target", "vercel")
if target_type == "vercel":
return VercelDeployment(project_id, resolved_config)
raise ValueError(f"Unknown deployment target: {target_type}")
And a VercelDeployment that implements the two methods using the Vercel API.
That's the entire abstraction. About 40 lines of interface code.
Why Not Just Call Vercel Directly?
The blog pipeline has one responsibility: take approved content and put it live. Vercel is the current target, but the deployment destination isn't a decision the pipeline should care about. The pipeline should care about: did the deploy succeed or not, and what's the URL.
Coupling the pipeline to Vercel's API directly would mean:
- Every deployment target change requires modifying the pipeline
- Testing deployment logic requires Vercel credentials or mocking their API surface
- Adding a new target means touching code that has nothing to do with the new target
The pipeline decides what to deploy. The target decides how.
The One Condition
Here's the part that matters: this was worth doing because we already knew the abstraction boundary existed, even with only one concrete case.
The blog pipeline produces static content that needs to be published to a web server. That's a generic requirement. Vercel is one way to fulfill it. But the content doesn't know or care about Vercel's API format, deployment hooks, or environment variables. It just needs to end up at a URL.
When you know the abstraction boundary because of the nature of the problem — not because you're guessing about future requirements — building it first is the right call. When you're guessing ("we might need to deploy to S3 someday"), YAGNI applies.
We knew the boundary existed because deploying static content is a solved problem with many valid solutions. The abstraction wasn't predicting the future. It was acknowledging a known category.
What It Unlocks
The factory pattern means adding a new target is a new file and one line in the registry. Cloudflare Pages, S3, self-hosted Docker — each becomes a class that implements two methods. The pipeline never changes.
This also means the blog pipeline can be tested independently of any deployment platform. Unit tests pass a mock deployment target and verify the pipeline calls deploy with the right data. No API calls, no credentials, no network.
The Trade-off
The cost is minimal: one extra file, one factory function, a few extra import paths. The benefit is that deployment is treated as an interface, not an implementation detail.
Most "abstractions are premature" warnings are about guessing future requirements and building for them. This wasn't a guess. The requirement existed — it just hadn't been implemented yet.
There's a difference between preparing for a hypothetical future and modelling an actual concept. The concept was deployment. We modelled it. Everything else followed.
Frequently Asked Questions
Does this work with Netlify?
The abstraction supports it, but we haven't implemented the Netlify target yet. Vercel covers current needs. The implementation would follow the same interface — submit build, poll status, return URL.
What about self-hosted Docker deploys?
Same interface. The self-hosted implementation would rsync or push a Docker build instead of calling a REST API. The pipeline wouldn't need to change.
How do you handle platform-specific features?
The config dict supports platform-specific settings. The abstraction defines the common contract. Anything unique to a platform lives in its implementation class and config schema.
Isn't this just an Adapter pattern?
Yes. Standard GoF adapter. The value isn't the pattern — it's applying the pattern to the right boundary.