"Is serverless secretly locking us in?" is the question Elena van Engelen opens with, and her answer is a qualified no. Lock-in is real, but it comes from letting cloud SDK calls leak into domain and application code, not from choosing Functions as a Service. Her thesis is that architecture, rather than avoidance, is the lever: if the layers that hold business rules have no compile-time knowledge of any cloud, the same logic can be deployed to more than one provider while still using each provider's native managed services.
Van Engelen is a senior software engineer specializing in Kotlin and cloud-native systems, a lead engineer at AZL (the life and pensions arm of the NN Group), and the author of Kotlin Crash Course. She presented this 49-minute talk, including a live coding demo and audience Q&A, at InfoQ Dev Summit Munich 2025; InfoQ published the recording and transcript on July 27, 2026. These notes report what she demonstrated and argued; where I add background the talk assumed, I label it as context.
What You Will Learn
- Why serverless lock-in is mostly a code-structure problem rather than a platform problem.
- Where Spring Cloud Function genuinely abstracts the runtime and where you still write provider-specific code.
- How AWS Lambda and Azure Functions differ in where the event trigger is declared, and why that difference matters architecturally.
- A simplified three-layer clean architecture sized for single-responsibility functions rather than large services.
- How Gradle modules turn an architectural guideline into a compile-time constraint.
- How Terraform CDK lets one language describe infrastructure for two clouds, and what its synth step costs in a pipeline.
- Where portability stops: non-functional concerns such as observability, permissions, and cold start remain provider-specific.
Serverless: What It Removes and What It Changes
Serverless does not mean there are no servers; it means you do not manage them. You do not patch operating systems, scan container images, or size hardware. You pay on a used-resource basis, scale automatically, and react to events. Functions as a Service (FaaS) is the compute form of this: a function takes an input, produces an output, and ideally carries one responsibility. That granularity is the practical reason to care. A container-based microservice or monolith bundles a lot of logic into one deployable, so a spike against one endpoint forces you to scale the entire unit; with FaaS, only the functions receiving load scale.
Van Engelen lists the use cases she considers well suited: REST APIs, IoT event processing, data transformation, clickstream processing, and scheduled tasks — the last on purely economic grounds, since keeping a container alive all day for a job that runs twice is waste. On REST APIs she confronts the standard objection. Cold start, the latency penalty when a platform must initialize a new execution environment, is why many teams reject FaaS for user-facing APIs. Her position is that the major providers have mitigations: AWS SnapStart combined with JVM priming can, in her experience, remove the cold start even at p99, and Azure's Elastic Premium plan mitigates it at a cost. Both are her reported experience rather than benchmarks presented in the talk, and Elastic Premium explicitly trades the pure pay-per-use model for pre-warmed capacity.
Context: SnapStart snapshots an initialized execution environment and restores from it; "priming" means deliberately exercising initialization paths — class loading, connection setup, framework wiring — before the snapshot is taken, which is why it pairs well with a heavyweight framework such as Spring. Her scale anecdote comes from PostNL, the largest parcel-delivery company in the Netherlands, where she previously worked: roughly 800 million events per day on AWS serverless. She offers it as evidence the model holds at volume, not as a published case study.
Spring Cloud Function: A Real but Partial Abstraction
Spring is a dependency-injection and application framework widely used on the JVM. Spring Cloud Function lets a Spring application run inside a FaaS runtime, so you keep dependency injection and auto-configuration instead of hand-rolling wiring inside a bare handler. Adapters exist for AWS Lambda and Azure Functions among others, and you can write your own if your provider is unsupported. Van Engelen is candid that the marketing claim of platform independence does not survive contact with deployment. The adapter dependency itself is provider-specific, and the entry point differs structurally:
| Concern | Azure Functions | AWS Lambda |
|---|---|---|
| Adapter dependency | Azure Spring Cloud Function adapter | AWS Spring Cloud Function adapter |
| Entry point | Kotlin function annotated with a function name | Class using the AWS SDK request and response types |
| HTTP trigger declaration | In the code, alongside the entry point | In the Infrastructure as Code, via API Gateway |
| Framework wiring | MAIN_CLASS on the function app |
MAIN_CLASS, SPRING_CLOUD_FUNCTION_DEFINITION, and a handler class from the adapter |
The asymmetry is the point. On AWS, Lambda must additionally be told which
function bean to invoke via SPRING_CLOUD_FUNCTION_DEFINITION — uploadDocument
in her demo — plus a handler pointing at a class inside the adapter. Because
the shape of this glue differs per provider, van Engelen's conclusion is that
entry points and triggers must be kept out of business logic entirely. Spring
Cloud Function buys a consistent programming model for the logic; it does not
erase the boundary.
The Demo Application
Rather than a Hello World, she models a workflow from writing her book: each finished chapter was emailed to reviewers with a filename encoding its review stage — editorial, then technical, then final editorial — with an awkward exception path if a chapter already marked final needed another change. The automation target is that manual, stage-tracked loop.
The application accepts a document upload over HTTP, validates it against publisher rules (her examples: image quality, word count), stores it, and then — triggered by the storage write — performs an automated review and emails a reviewer a secure link. Both clouds run identical business logic against their own native services.
| Capability | Azure | AWS |
|---|---|---|
| HTTP entry | Azure Functions HTTP trigger | API Gateway to Lambda |
| Compute | Azure Functions | AWS Lambda |
| Storage | Blob storage | S3 |
| Storage-triggered compute | Azure Function on Blob event | Lambda on S3 event |
| Azure Communication Services | Simple Email Service (SES) |
Note that she is not avoiding managed services in the name of portability. She uses each provider's native storage, eventing, and email. Portability is achieved by isolating those calls, not by refusing them. Both HTTP endpoints are protected only by an API key, which she flags as deliberately minimal demo security — adequate to show the flow, but not what you would ship for an endpoint accepting uploads and issuing links to stored documents.
A Clean Architecture Sized for Functions
Clean architecture is the layered style in which dependencies point inward toward the domain, so that policy does not depend on mechanism. Van Engelen uses a deliberately simplified version, arguing that a function with one responsibility does not need the layer count a large container-based service might justify.
- Domain — domain objects and basic domain validation. In the demo, document metadata. Depends on nothing.
- Application — use cases, business logic, and the interfaces through which that logic reaches the outside world. In the demo: a document validation and save service, and a review-and-notify service. Depends on domain only.
- Infrastructure — all cloud-specific code: entry points, triggers, and the implementations of the application-layer interfaces. Depends on application and domain.
Her argument for keeping domain rules free of cloud coupling is drawn from pensions, where the rules are genuinely intricate. Life events change entitlements in ways that persist for decades: in the Netherlands a marriage must be registered because a later divorce can transfer part of a pension to the ex-partner, who then becomes a participant in their own right. Rules of that complexity outlive infrastructure decisions, which is the real case for keeping them portable — longer application lifetime and freedom to move, not multi-cloud for its own sake.
Two interfaces carry the whole demo. An object-storage interface is implemented by an S3 adapter and a Blob storage adapter; a notification interface is implemented by an SES sender and an Azure Communication Services sender. The application layer calls the interfaces and never learns which cloud answered. The same object-storage interface also generates the secure URI embedded in the reviewer's email, so even signed-URL generation stays behind the abstraction. She is honest about what is real and what is a placeholder: validation simply returns true, and the "review" is a random string standing in for what could be an AI model or publisher-specific logic. She used AI assistance while writing the infrastructure glue and predicts such adapter boilerplate is a natural candidate for generation, while noting mid-demo that it did not always produce what she wanted.
Gradle Modules as an Enforcement Mechanism
The layering only holds if violating it is impossible rather than merely
discouraged. Van Engelen maps each layer to a separate Gradle module and lets
declared dependencies enforce direction: settings.gradle lists the modules, and
each module's build file declares only its permitted dependencies. Application
depends on domain; infrastructure depends on application and domain; nothing
depends outward. Code that reaches from application into infrastructure does not
compile. Her reasoning is candidly human: under delivery pressure developers cut
corners, and an unenforced guideline erodes over time. A build failure is a
cheaper correction than an architecture review. There is a packaging benefit too
— because AWS and Azure live in separate infrastructure modules, each artifact
carries only the adapter code it needs, and the CDK module is excluded from
deployment artifacts entirely since it only describes infrastructure for the
pipeline.
Architecture And Data Flow
flowchart TD
subgraph Portable["Portable modules: no cloud dependencies"]
DOM[Domain: document metadata and validation rules]
APP[Application: validate-and-save, review-and-notify]
IFACE[Interfaces: object storage, notification]
APP --> DOM
APP --> IFACE
end
subgraph AWSMOD["AWS infrastructure module"]
AWSENTRY[Lambda entry points: uploadDocument, processDocument]
S3IMPL[S3 storage adapter]
SESIMPL[SES email adapter]
end
subgraph AZMOD["Azure infrastructure module"]
AZENTRY[Azure Functions: HTTP upload, blob-triggered review]
BLOBIMPL[Blob storage adapter]
ACSIMPL[ACS email adapter]
end
AWSENTRY --> APP
AZENTRY --> APP
IFACE -. implemented by .-> S3IMPL
IFACE -. implemented by .-> SESIMPL
IFACE -. implemented by .-> BLOBIMPL
IFACE -. implemented by .-> ACSIMPLThe runtime sequence is identical on both clouds, with different services bound by dependency injection at startup. A client POSTs a document to the HTTP endpoint, which invokes the upload entry point; that entry point calls the application service, which validates and saves through the object-storage interface. The resulting storage event triggers the second function, whose entry point calls the review-and-notify service. That service produces a review, asks the object-storage interface for a secure URI, and sends both through the notification interface to the human reviewer.
Deployment with Terraform CDK
Van Engelen deploys with Terraform CDK (CDKTF), which lets you write infrastructure in a general-purpose programming language that then generates Terraform configuration. Her reasons for choosing it over a provider-native CDK are specific to the multi-cloud goal: one toolchain and language for both providers, reuse of existing Terraform modules regardless of the language they were authored in, and previewable change plans. She frames the shared vocabulary as one language in two dialects — the concepts and workflow are identical, but you still address AWS resources on AWS and Azure resources on Azure, so the code is not literally shared. She concedes CDKTF is more verbose than the native AWS CDK and that she would use the native CDK on a single-cloud project. IAM permissions — allowing a Lambda to reach S3, or API Gateway to invoke a Lambda — are declared explicitly here, one more reason permissions are not portable.
Two pipeline details are worth carrying away. First, cdktf get fetches provider
bindings and cdktf synth generates the Terraform files, and synth is slow — run
it only when the infrastructure definition changes. For a code-only change,
terraform plan and apply against the already-synthesized configuration is
much faster and enough to push new function artifacts. Second, her GitHub Actions
setup runs two pipelines from a single commit, one per cloud, each building the
package, uploading it to provider storage, and applying Terraform. Azure took
noticeably longer to deploy than AWS during the live demo.
Why Kotlin Helps
Kotlin is not required for the pattern, but van Engelen gives a concrete operational reason to prefer it on FaaS: Kotlin can target older JVM bytecode levels, from Java 8 upward, decoupling your language version from the runtime a provider offers. When AWS supported Java 21 before Azure did, a Java codebase already on 21 would have faced a migration problem on the lagging provider; Kotlin let her keep the latest language version regardless of the JVM on each platform. That matters in any multi-cloud setup where runtime support arrives at different times.
Trade-offs And Limitations
The pattern is not free, and van Engelen's own framing plus the Q&A surface most of the costs.
- Portability is partial by construction. Adapter dependencies, entry points, trigger definitions, IAM policies, and Infrastructure as Code remain provider-specific. What travels is the domain and application layers, which she argues is where the durable value lives.
- Non-functional concerns still diverge. Asked about observability, she was clear that the logging API in your code stays generic — use a standard logging facade, never a cloud SDK logger — but routing those logs to CloudWatch or an Azure equivalent is configured in the infrastructure layer and the IaC, and differs per cloud. She noted this is true even without multi-cloud ambitions. She showed no worked example of unified tracing or metrics across providers, so treat cross-cloud observability parity as unproven by this talk.
- Abstraction naming leaks intent. An audience member pushed back on the
interface being named for object storage, arguing a domain-facing abstraction
should say what the business needs, not what the technology is, since S3 is
itself an API that Blob storage could stand in for. Van Engelen agreed:
save,store, orpersistis better, because the implementation could equally be a Cosmos DB document. A leaky name today becomes a leaky assumption later. - Cold start is mitigated, not eliminated for free. SnapStart with priming and Elastic Premium both add configuration work, and Elastic Premium adds cost. Her p99 claim is her own experience with a specific setup.
- More modules, more build complexity, and two clouds mean two of everything operationally: two pipelines, two sets of credentials and permissions, two deployment latencies, duplicated infrastructure code. The talk demonstrates feasibility; it does not argue that running production on both clouds simultaneously is worth the ongoing cost.
- The demo's logic is intentionally trivial. The architecture is the deliverable, not the algorithm.
Context worth adding, since the talk touches it only indirectly: the demo emails a secure link to a stored document. A real version needs deliberate decisions about signed-URL expiry, recipient verification, and whether documents carry regulated data — precisely the kind of concern that, like observability, resolves differently on each cloud.
Practical Takeaways
- Treat lock-in as an architectural property. As van Engelen points out, a container that calls S3 is just as coupled as a Lambda that calls S3; containers are not inherently more portable.
- Define ports in the application layer that describe business intent — save, notify, persist — and put every SDK call, including logging, behind an implementation in an infrastructure module.
- Keep triggers and entry points out of business logic. Their shape differs by provider and, on AWS, partly lives in Infrastructure as Code rather than code.
- Enforce layer direction with build tooling — Gradle modules here, but the same discipline applies to Maven modules, .NET projects, Go packages, or lint rules. If a violation only fails review, it will eventually ship. Package per target so each artifact carries only the adapters it needs.
- Bind cloud-specific implementations through dependency injection at startup; compile-time DI works equally well if your stack offers it.
- With CDKTF, skip
synthon code-only deploys. Prefer a provider's native CDK when you target a single cloud. - Choose a language that decouples you from provider runtime upgrade schedules when targeting more than one platform.
Key Terms
- Serverless — a model where the provider operates the infrastructure; you pay for resources used and code responds to events.
- Function as a Service (FaaS) — event-triggered compute for small, single-responsibility functions that scale independently, such as AWS Lambda and Azure Functions.
- Cold start — the added latency when a platform must initialize a new execution environment before running your code. AWS SnapStart (with priming) and Azure Elastic Premium are the two mitigations discussed.
- Spring Cloud Function — a Spring project that lets a Spring application run as a cloud function through provider-specific adapters.
- Dependency injection — supplying a component's collaborators from outside rather than constructing them internally, which is what allows one business service to bind to different cloud implementations.
- Clean architecture — layered design where dependencies point inward toward the domain, so business rules do not depend on delivery or infrastructure mechanisms.
- Terraform CDK (CDKTF) — a toolkit for writing Infrastructure as Code in a
general-purpose language;
cdktf synthgenerates the Terraform files thatplanandapplythen act on.
Closing Assessment
None of the building blocks are new, and van Engelen says so plainly: clean architecture, dependency inversion, and module boundaries are decades old. The contribution is the demonstration — a running system on two clouds showing that FaaS does not force provider coupling into business logic, and that the boundary can be enforced by the build rather than by discipline. For most teams the value is not actually running on two clouds, but retaining the option and keeping long-lived business rules readable and testable without a cloud account.
Reference: Elena van Engelen, Clean Architecture for Serverless: Business Logic You Can Take Anywhere, InfoQ Dev Summit Munich 2025, published by InfoQ on July 27, 2026.