Presentation: Clean Architecture for Serverless: Business Logic You Can Take Anywhere
Elena van Engelen ยท rss ยท 2026-07-27 source โ Reader ๐ฅ๏ธ
Clean Architecture (Ports & Adapters / Hexagonal Architecture)Cloud Vendor Portability & Lock-in AvoidanceEvolutionary Architecture & Fitness FunctionsModularity & Coupling
Structuring Function-as-a-Service applications with a simplified Clean Architecture โ a domain and application layer with zero cloud dependencies, whose separation from infrastructure is enforced not by convention but by Gradle module boundaries, wired to cloud entry points via Spring Cloud Function's dependency injection โ lets a team write business logic once and deploy it, materially unchanged, to multiple serverless cloud providers, eliminating vendor lock-in without giving up native cloud capabilities.
practical ยท mixed โ A conference talk arguing for a specific course of action (structure your FaaS code this way to get portability) โ practical, not theoretical. Form is mixed: expository explanation of serverless/Clean Architecture/Terraform CDK, two personal narratives (the book-editing workflow, the Dutch pension life-events domain) used as motivating examples, a live demo that functions as empirical demonstration/argument, and an argumentative Q&A where the audience pressure-tests and refines the speaker's abstraction boundaries in real time.
Structure
| Part | Role |
|---|---|
| Opening provocation: "Is serverless secretly locking us in?" | context / motivation |
| What serverless/FaaS is, its resource-efficiency case, common use cases, and cold-start mitigation (AWS SnapStart, Azure Elastic Premium) | context โ establishes that serverless is worth using on its own merits, so the lock-in objection is the only thing left to answer |
| Spring Cloud Function explainer, including the admission that despite being marketed as platform-independent, deploying to different clouds still requires cloud-specific adapter code and trigger definitions | sets up the actual problem โ the framework alone does not deliver full portability |
| Hello World demo on AWS and Azure, showing where cloud-specific code actually lives (adapters, entry points, Infrastructure-as-Code triggers) | evidence โ localizes the cloud-specific surface area before proposing to isolate it |
| The book-editing-workflow narrative (manual chapter review process) reframed as the demo application's use case | narrative motivation for the concrete example that follows |
| The Dutch pension life-events narrative (participants, beneficiaries, marriage/divorce/death rules) | narrative โ argues by example that real business logic can be complex, long-lived, and jurisdiction-specific, which is precisely why it should not be entangled with any one cloud's infrastructure |
| The Clean Architecture layering (domain / application / infrastructure) and its enforcement via Gradle module dependency configuration | the core prescriptive mechanism โ this is the actual solution being proposed |
| Live demo: the same business logic wired into both an AWS Lambda entry point and an Azure Function entry point via dependency injection, deployed with Terraform CDK, and exercised end-to-end (upload, validate, save, review, email) | evidence / proof of concept for the central claim |
| Key takeaways: portability is achievable; Spring Cloud Function gives runtime DI; Gradle enforces layering; Kotlin's JVM-version flexibility helps; Terraform CDK helps for multi-cloud | conclusion โ restates the unity as a checklist of mechanisms |
| Q&A: storage-abstraction naming (Participant 1) and cross-cloud observability/logging (Participant 2) | elaboration and correction under audience scrutiny โ refines where the architectural boundary actually should sit, and extends the core claim to a domain (observability) the talk itself hadn't covered |
Key terms
- Clean Architecture (as used here)
A simplified, three-layer variant โ domain, application, infrastructure โ appropriate to a single-responsibility function, not the full classical set of concentric rings from the canonical formulation. - Business logic
Domain objects, domain validation, and application-layer use cases โ code that has zero dependency on any cloud SDK and is meant to be identical regardless of where it runs. - Infrastructure (layer)
Cloud-specific glue code: function triggers, entry points, and calls into concrete cloud SDKs (Blob storage, S3, SES, ACS) โ deliberately disposable, replaceable per cloud, and expected to eventually be machine-generated. - Portability / cloud-agnostic
The property that the same business-logic artifact, unmodified, runs correctly once wired into a different cloud's entry point and infrastructure implementations โ demonstrated, not just claimed, via the live dual-cloud demo. - Gradle module (as enforcement mechanism)
A build-tool-level dependency declaration (in settings.gradle / module build files) that makes violating the dependency rule a compile failure, not just a style guideline. - Terraform CDK (CDKTF)
A layer over Terraform that lets you author infrastructure definitions in a general-purpose language (here, Kotlin) and generate/reuse the underlying Terraform config across languages and, in her case, across cloud providers.
Propositions
- explicitCold start, historically FaaS's biggest objection for latency-sensitive workloads like REST APIs, has been effectively mitigated by cloud-native features (AWS SnapStart with JVM priming; Azure Elastic Premium), so it should no longer be treated as a blocking objection to serverless.conf: high
- explicitFunction-as-a-Service is more resource-efficient than container-based monoliths or microservices, because only the specific function handling a given event type needs to scale, not the entire application.conf: high
- explicitFrameworks that market themselves as 'platform independent' (e.g. Spring Cloud Function) still require developers to write cloud-specific code for triggers and entry points when deploying to different providers, so the framework alone does not deliver full portability.conf: high
- explicitcorroboratedGenuine business-logic portability across cloud providers requires physically separating code into layers by dependency direction โ a domain layer with no dependencies, an application layer that depends only on domain, and an infrastructure layer that depends on both โ so cloud-specific code is confined to a thin, replaceable outer layer.conf: highsupportsYou Outsourced the AIโbut You Still Own the Risk ยท p6 โ Concretely instantiates the abstract 'architect for modularity, keep infrastructure vendor-neutral' hedge โ applied here to cloud-infrastructure vendor lock-in via Clean Architecture layering and Gradle-enforced dependency direction, rather than to the AI-model vendor lock-in the other piece addresses.supportsYou Outsourced the AIโbut You Still Own the Risk ยท p10 โ The working live demo โ the same business logic deployed unmodified to both AWS and Azure โ is direct evidence that engineered portability is achievable at least at small scale, addressing a premise the other piece asserts as load-bearing but leaves unproven.refines โArticle: Modular Edge Computing at Multi-Tenant SaaS Scale on Cloudflare Workers ยท p6 โ Complicates the claim that genuine cross-cloud portability just requires physically separating code into layers by dependency direction: Akamai vs. Cloudflare differ at the execution-model level itself (fetch-handler owning the request vs. a lifecycle-event handler that can only set a property variable), a gap that layered isolation of I/O dependencies (storage, logging, triggers) does not touch.supports โForsvaret med egen data-grunnlov ยท p3 โ Both converge on the same architectural move โ isolate the stable, portable core (data / business logic) from the volatile, vendor-specific periphery (technology / infrastructure layer) โ to buy switching freedom, in a defense-architecture context rather than a serverless-coding context.extends โPresentation: How To Run on Three Clouds at Once, and When Not To ยท p3 โ Both pursue cloud-vendor-portable architecture to avoid maintaining per-provider software versions, but Form3's case extends the idea from deploy-time portability (same FaaS code deployable to two providers) to simultaneous, always-on runtime portability (one logical platform actively serving from three providers at once).
- explicitA simplified, lightweight version of Clean Architecture โ not its full classical layering โ is sufficient for FaaS code, because a single-responsibility function does not carry the complexity a container-based monolith needs the full layering to manage.conf: high
- explicitdevelopedEnforcing the architecture's dependency direction at the build-tool level (Gradle module dependency configuration) is more reliable than relying on developer discipline or code review, because violating code will not compile at all.conf: highextendsArchitecture as Code to Teach Humans and Agents About Architecture ยท p1 โ Gradle module dependency configuration is a stronger, prevention-time version of 'architecture as code': rather than only giving fast feedback when implementation diverges from design, it makes the divergent code physically uncompilable.extends โWorse is better: C# versus F# ยท p10 โ Both propositions agree that enforcing a dependency-direction rule mechanically beats relying on developer discipline; Seemann pushes the enforcement point one level down, from build-tool/module-boundary enforcement to language-compiler enforcement of acyclicity itself.
- explicitKotlin's ability to target JVM versions as old as 8 gives a portability advantage over being tied to whichever JVM version a given cloud provider currently supports, avoiding forced migrations when providers upgrade at different paces (e.g. AWS moving to a newer JVM before Azure).conf: high
- explicitTerraform CDK is preferable to hand-written per-cloud Terraform when the goal is multi-cloud deployment, because it lets teams define infrastructure once in a general-purpose language and reuse it โ even across host languages โ while still generating ordinary Terraform underneath.conf: high
- explicitInfrastructure-as-code changes are comparatively rare and slow to synthesize, so CI/CD pipelines should separate a slow 'regenerate Terraform from CDK' step from a fast 'deploy new application code only' step, running the former only when infrastructure actually changes.conf: medium
- explicitStorage and persistence should be named and interfaced generically in business logic (e.g. 'save', 'store', 'persist') rather than after the concrete backing technology ('upload to S3', 'upload to Blob storage'), so the interface itself does not leak which cloud or which kind of store is used underneath.conf: high
- explicitCross-cloud observability portability follows the same pattern as storage portability: business logic should call a generic logging API, and only the infrastructure layer's configuration determines which cloud-specific sink (e.g. CloudWatch) receives the logs, so no business-logic code needs to change between clouds.conf: high
- explicitBoilerplate infrastructure-layer code (wiring calls to a specific cloud's storage or email SDK) is mechanical enough that the author expects it to increasingly be AI-generated, since it requires no business judgment.conf: medium
- implicitWhether a piece of logic belongs in the infrastructure layer is determined by whether using it requires knowing a concrete cloud or technology, not by whether it merely sounds 'technical' โ so a name like 'upload to object storage' already leaks a detail the architecture was supposed to hide, while 'save' or 'persist' does not.conf: medium
- implicitdevelopedBeing able to deploy the same business logic to two cloud providers simultaneously is itself worth the architectural overhead (extra interfaces, module boundaries, DI wiring, CDK complexity) for most FaaS applications, independent of how often an organization would actually exercise that capability rather than merely retain the option.conf: mediumrefines โPresentation: How To Run on Three Clouds at Once, and When Not To ยท p23 โ extract-clean-architecture-for-serverless#p14 asserts multi-cloud deployability is 'itself worth' its architectural overhead; Form3's talk refines that claim with a more skeptical, ROI-driven framing โ the overhead is worth it specifically because and only because a paying market demanded it, not as an intrinsic architectural good.
Arguments
You can achieve real business-logic portability across serverless cloud providers, without sacrificing native cloud capabilities, by separating business logic (domain + application) from infrastructure code via a Clean-Architecture-style dependency rule enforced at the Gradle module level.
Explicit premises:
- FaaS functions have a single responsibility, so a simplified (not full) Clean Architecture layering is enough.
- Domain and application layers have zero cloud dependencies; the infrastructure layer holds all cloud-specific code (triggers, storage SDKs, email SDKs).
- Gradle module dependency configuration physically prevents infrastructure code from leaking into domain/application โ the build fails if it does.
- Business logic is injected into cloud-specific entry points (the AWS Lambda handler, the Azure Function) via dependency injection (Spring Cloud Function) rather than written inside them.
Implicit premises:
- The parts of an application that vary between clouds are confined to entry-point/trigger wiring and to calls that touch actual cloud SDKs โ everything else (validation rules, domain objects, orchestration order) is cloud-independent by nature.
- Compiler/build-enforced boundaries are a more durable guarantee of architectural conformance than code review or documented convention, especially under time pressure.
Inferences:
- The same technique should generalize to other backing services beyond storage and email (queues, secrets, config) as long as they are accessed through hand-rolled interfaces rather than directly through a cloud SDK.
Terraform CDK is the right infrastructure-as-code choice specifically when deploying the same conceptual infrastructure to multiple cloud providers.
Explicit premises:
- CDK lets you write infrastructure definitions in a general-purpose language and reuse/generate Terraform config for different providers.
- It brings developers into infrastructure work more easily than raw Terraform HCL, helping DevOps in practice.
- Terraform modules are reusable and can be authored once and consumed from CDK in different host languages.
Implicit premises:
- The extra abstraction and verbosity of CDKTF (versus a provider-native CDK like the AWS CDK) is worth paying specifically for the multi-cloud case, even though the speaker herself prefers the native CDK for single-cloud work.
Inferences:
- Teams targeting only one cloud provider should prefer that provider's native IaC tool over Terraform CDK, per the speaker's own admission.
The interface boundary between business logic and infrastructure should be drawn at 'requires knowing a concrete technology,' not at 'sounds technical' โ so an operation like 'save a document somewhere' belongs on the business-logic-facing interface even though persistence sounds like an infrastructure concern.
Explicit premises:
- The original interface/function name ('upload to object storage') implicitly named a technology-classified concept.
- Participant 1 argued the abstraction should hide even which kind of store (object store vs. database) is used, not just which cloud.
- The speaker agreed and reframed the interface as 'save' / 'persist', callable without knowing the cloud or store type.
Implicit premises:
- Any name that hints at the concrete technology behind an interface is itself a portability leak, even when the implementation is already swapped correctly per cloud.
Inferences:
- The same audit should be applied to every other infrastructure-facing interface in the talk (email sending, logging) to check for hidden concreteness the speaker did not examine live.
Narratives
- The speaker's own book-writing/editing workflow โ each chapter renamed to track review stage, emailed between author, editorial reviewer, and technical reviewer, with a manual exception path if an already-finalized chapter needs revisiting.
โ Motivates why you'd want to automate a 'validate โ save โ review โ notify' workflow with FaaS event triggers, and grounds the abstract demo application in a real, personally experienced manual process rather than an invented toy example. - The Dutch pension domain: participants and pension funds as domain objects, and 'life events' (marriage, divorce, death) that must be registered because they change who has a legal claim on a pension, sometimes decades later.
โ Illustrates, by example, that real business logic can be complex, long-lived, and jurisdiction-specific โ implicitly arguing for the value of the dependency rule, since logic this consequential and durable should not be entangled with any one cloud vendor's infrastructure choices. - The live cross-cloud demo: uploading chapter 13 via Postman to the AWS endpoint and chapter 8 to the Azure endpoint, watching each get validated, saved to S3/Blob storage respectively, reviewed (via a placeholder random-string 'AI review'), and emailed back with a secure link โ using the same business logic in both cases.
โ Functions as the empirical proof-of-concept for the unity and for proposition 4/argument 1 โ it is the demonstration that the described architecture actually delivers the promised portability, not just an argument for why it should.
Questions addressed
- โ
Does using serverless secretly lock you into a cloud vendor?
Not necessarily โ vendor lock-in is a consequence of how you structure your code, not an inherent property of serverless, if business logic is architecturally separated from infrastructure. - โ
How do you keep FaaS business logic maintainable without pulling in a full container-app's worth of architectural layers?
Use a simplified, three-layer version of Clean Architecture appropriate to a single-responsibility function. - โ
How do you physically prevent business logic from depending on cloud-specific code, rather than just asking developers to follow a convention?
Enforce the dependency direction at the Gradle module level so violating code fails to compile. - โ
Exactly where should the line be drawn between 'business/domain' and 'technical/infrastructure' โ e.g., is a storage abstraction domain logic or infrastructure?
The line should be drawn at whether using the interface requires knowing the concrete cloud or technology behind it, not at whether the operation sounds technical โ but this resolution only emerged through Q&A negotiation with Participant 1, not from the talk's own upfront design. - โ
Does this approach also work for cross-cloud observability/logging, not just storage and business data?
Yes, via the same pattern: a generic logging API in business-logic code, with the cloud-specific sink (e.g., CloudWatch) configured only in the infrastructure layer. - โญ Is the added architectural overhead (extra interfaces, module boundaries, DI wiring, CDK complexity) worth it for teams that will likely never actually run the same function on two providers simultaneously?
Gaps
- incompleteThe talk demonstrates portability for a toy two-function application with exactly one storage interface and one email interface; it doesn't show how the pattern holds up with dozens of business services, multiple storage backends, queues, and secrets managers, where interface proliferation and dependency-injection wiring complexity could become their own maintenance burden.
Read next: Look for case studies or post-mortems of large-scale multi-cloud Clean-Architecture systems at real organizations, to see whether the pattern holds under complexity or only at demo scale. - incompleteThe cost of this approach relative to its benefit is never argued. Extra interfaces, Gradle module boundaries, DI wiring, and CDK complexity have a real engineering cost, but the talk never weighs that against the โ for many teams, mostly hypothetical โ benefit of actually being able to switch or dual-run cloud providers, when most organizations pick one primary cloud for reasons (pricing, existing platform investment) unrelated to code portability and rarely execute a true migration even when the code is portable.
Read next: extract-outsourced-ai-still-own-the-risk#p10 raises the structurally identical unresolved premise in the AI-vendor-lock-in context ('portability is achievable enough to function as a real hedge') โ worth reading together to test whether 'engineered portability' claims generalize across domains or share the same unexamined cost side. - uninformedThe Q&A's observability answer treats 'generic logger + cloud-specific sink config' as basically solving cross-cloud observability, but doesn't address the harder problems: correlating a single business transaction's logs, traces, and metrics when parts of it run on AWS and parts on Azure, distributed tracing across clouds, or cost/retention differences between CloudWatch and Azure Monitor.
Read next: extract-infoq-data-driven-platform-engineering (platform engineering and observability federation across a real multi-cloud platform) and writing on OpenTelemetry's cross-cloud adoption as the actual mechanism for correlating traces, not just directing log lines. - illogicalThe talk frames Terraform CDK's cross-cloud reuse as a portability win ('same lingo... different dialect'), while simultaneously showing that the actual resource definitions are provider-specific (an AWS Lambda + API Gateway + IAM permission block looks nothing like an Azure Function App block). What's actually portable is the tooling/metalanguage (write infra in Kotlin instead of HCL), not the infrastructure-as-code itself โ only the business-logic layer, not the infrastructure layer, is genuinely portable. The talk doesn't name this distinction, which risks readers concluding CDKTF gives infrastructure portability it does not.
Read next: Compare with writing that distinguishes tooling-level multi-cloud consistency (shared IaC language) from artifact-level portability (the same deployable config running on either cloud) โ e.g. Gregor Hohpe on multi-cloud strategy and exit costs.
Related documents
- Architecture as Code to Teach Humans and Agents About Architecture
- You Outsourced the AIโbut You Still Own the Risk
- Agents don't know what good looks like. And that's exactly the problem.
- Article: Modular Edge Computing at Multi-Tenant SaaS Scale on Cloudflare Workers
- Three more static code analysis sensors
- The Hard-Stop Rule: From 3 HCM Monoliths to 120 Domain Microservices
- Worse is better: C# versus F#