DEV Community

Cover image for Decoupling Lambda: Building Portable, Deployment-Agnostic Backends for the Real World
walid m'sallem
walid m'sallem

Posted on

Decoupling Lambda: Building Portable, Deployment-Agnostic Backends for the Real World

Cloud-native development has made it easy to spin up scalable APIs using AWS Lambda. As teams move from prototypes to production-grade systems, they often inherit a tangled web of tightly-coupled functions, infrastructure dependencies, and vendor lock-in.

This article outlines how we approached the challenge of building deployment-agnostic backend services that can run in Lambda, Docker, Kubernetes, or even on-prem environments without major code changes, drawing on a real-case scenario in ComplyCube.

Rethinking How We Build Lambda Backends

AWS Lambda is an attractive entry point, with many developers opting for it. For development teams focusing on speed and modularity, Lambda can be promising due to several reasons:

  • Rapid and seamless deployments.
  • Simple ownership.
  • One function per endpoint.

However, as projects start to mature, these early benefits can quickly become operational burdens. Several of the problems that may show up include:

  • Authentication, logging, and error handling are duplicated across functions.
  • Over 50 functions can evolve in isolation, creating inconsistent styles.
  • Local testing gets cumbersome, with each function needing its own setup.
  • As AWS-specific logic is pervasive, migration from Lambda can be a painful process.

The Real Problem: Lambda-Coupled Codebases

Most teams fail to see a business's long-term vision, focusing merely on writing Lambda functions rather than building enduring business logic. A well-designed UserService or DocumentService should be able to work anywhere in Lambda, including a Docker container, Kubernetes, or even run on-premise.

Yet, in practice, much of this logic is tightly coupled to AWS-specific abstractions. For instance, in most projects, the logic might include:

  • event.queryStringParameters or
  • context.awsRequestId

This results in lock-in for vendors at the code level, not just at the infrastructure level. Most teams write Lambda functions, not business features, creating unnecessary barriers to operational flexibility.

Code Independence (Decoupling Logic from AWS specifics)

Separating your business logic from AWS-specific details is essential for building truly portable systems. This approach allows your core code to stay clean and reusable, regardless of where it’s deployed or how it’s triggered. By keeping cloud-specific handling minimal and isolated, teams gain the flexibility to adapt quickly as needs evolve:

  • Write pure business modules that operate independently of runtime.
  • Keep AWS-specific handling (such as parsing event and context) inside minimal wrappers.
  • Use internal adapters to normalize events from different sources (HTTP, SQS, SNS) into a single, clean format that your business logic can consume.

Real-World Use Cases

Decoupling deployment from business logic isn’t just technical, it genuinely simplifies life for teams in complex, regulated industries. At ComplyCube, this flexibility keeps us responsive and agile in supporting a wide range of compliance and operational needs. Here’s how this flexibility comes to life:

  • Regulatory & Compliance Needs: As an award-winning leader in identity verification and anti-money laundering, ComplyCube must navigate complex data handling and storage requirements. Our deployment-agnostic backend lets us run the same logic on AWS, private clouds, or on-premises, so no rewrites are needed.
  • Operational Flexibility: Business needs can change overnight. Portable modules allow easy migrations, support for mixed deployments, or spin up private client instances, making changes straightforward, not stressful.
  • Vendor Independence: Separating core logic from infrastructure means we can quickly adapt to changes in cost, regulation, or client needs. If we must leave AWS or meet a unique enterprise need, we do so without costly rewrites.

Deployment Flexibility

Decoupling business logic from deployment gives you real control over how each feature is delivered. This means you can tailor your deployment approach for compliance, performance, or client needs. This flexibility allows teams to mix and match deployment styles to best fit any scenario.

  • Independent Lambdas for Precision: Deploying features as separate, focused Lambdas gives you precise control, think fine-grained IAM roles, isolated scaling, and ultra-fast cold starts. This is perfect for sensitive operations or compliance-heavy workflows where control is non-negotiable.

  • Grouped Modules for Simplicity: When related modules can be bundled together, a single Lambda streamlines deployment and minimizes operational complexity. This approach reduces the number of moving parts, making monitoring, testing, and updates far less daunting.

  • Config-Driven Customization: Flexible configuration files let you declare exactly which modules and controllers each handler should serve. Need to launch a new region, switch clouds, or adapt to a client's unique environment? Simply update the config, no code rewrites, no duplication, just seamless adaptability.

Lambda Package Optimization

Optimizing the size of each Lambda function is crucial for reducing cold start latency and improving performance. Keeping deployment packages lean means faster response times and lower operational costs. Here are the core practices that make this possible:

  • Each handler should only include exactly the code it needs, no more, no less.
  • Shared logic should be appropriately reused (not copied and pasted), and tools like Webpack can tree-shake unused code to keep package sizes small.
  • These optimizations enable handlers to stay around 7–15MB, ensuring faster cold starts and more efficient deployments.

Unifying Everything into One Clean Project

Another often underestimated benefit of deployment-agnostic architecture is how it enables a unified project structure. When business logic is separated from infrastructure, teams can consolidate features into a single, well-organized codebase. The benefits include:

  • All features (User, Documents, etc.) can live in one place, like a monorepo.
  • All features follow the same coding standard.
  • Shared logic (auth, logging, DB connection) lives in a single, shared library.
  • New developers see a consistent pattern across all features , resulting in faster onboarding. This combination (clean separation + monorepo-like structure) balances flexibility and maintainability.

Since your logic is a clean business code, you can now:

  • Run the whole backend locally like a normal app.
  • Run just one feature or run it locally if needed.
  • Test without AWS mocks because your tests hit the business logic directly.

Pros & Cons of Lambda Deployment Strategies

Choosing how to structure your Lambda deployments can greatly impact performance, maintainability, and operational complexity. There’s no one-size-fits-all answer—each approach has trade-offs. Here’s a breakdown of the most common strategies and what they mean in practice:

One Lambda per feature:

  • Pros: Smaller cold starts, fine-grained IAM roles, isolated scaling.
  • Cons: More deployment units to manage.

Single Lambda for all features or a group of modules:

  • Pros: Simple deployment, fewer moving parts.
  • Cons: Larger cold starts, risk of noisy neighbors (one feature can slow down others).

Group related modules under a single handler:

It allows you to balance flexibility and maintainability by reducing the number of deployments while keeping modules independent and easy to manage.

How ComplyCube Built a Flexible Backend

This section aims to demonstrate how we can apply the strategies above in a real-world scenario at ComplyCube. For our backend, we decided to opt for NestJS, as it fits these principles relatively well.

NestJS is modular by design and perfect for our feature isolation goal. Each feature (User, Documents, etc.) is a standalone Nest module that includes:

  • Controller (entry point).
  • Service (business logic).
  • DTOs (data contracts).
  • Shared utilities (DB, auth) were imported from a central place, making every feature self-contained but consistent.

Local Development & Testing

Consolidating business logic separately from AWS specifics greatly simplifies local development and testing:

  • Run your entire backend locally as a standard application, or test individual Lambdas with tools like serverless-offline.
  • Conduct tests directly against business logic without AWS mocks, streamlining and accelerating your testing cycles.
  • Developers benefit from both comprehensive integration tests and rapid, isolated feature tests, making the development experience flexible and efficient.

Challenges We Solved

Throughout our journey, we encountered a range of technical hurdles while building a modular, deployment-agnostic backend. Addressing these challenges helped shape a more robust and maintainable system. Here are some of the key obstacles and how we overcame them:

  • Managing shared utilities: Centralized all shared modules for consistency and easier updates.

  • Circular dependencies: Kept modules independent, relying only on shared utilities to avoid tangled dependencies.

  • Observability: Established consistent logging across all modules using a shared logger.

  • Scaling options: Enabled each feature to scale independently when needed.

  • Lazy loading: Cached and reused initialized modules to load only what was necessary per request, reducing overhead.

What We Achieved at ComplyCube

By adopting these principles, we realized significant improvements in both our development process and operational flexibility. The results reflect the benefits of a clean, modular architecture built for change. Here’s what we gained:

  • Clean separation: Business logic is entirely decoupled from Lambda-specific code.
  • Flexible deployment: Both per-feature and combined Lambda approaches are supported.
  • Unified standards: All features follow the same structure, ensuring maintainability.
  • Easy developer experience: Full support for local development accelerates onboarding and testing.

Final Thought "A Broader Vision Than Just NestJS"

We used NestJS because it fits this philosophy. But this is not a NestJS-only idea - it's about:

  • Treating features as first-class units.
  • Keeping logic clean and portable.
  • Making deployment a replaceable wrapper.

Any framework can work - if you apply the right architectural thinking.

Top comments (0)