Serverless Microservice with DDD, Onion Architecture in Nx/Monorepo (NestJS, AWS Lambda and AWS CDK)
Updated: Apr 7, 2022
Migrating from monolithic to microservice is a long battle in every developer's life. Since microservice is a different concept from monolithic and also needs to apply a bunch of new techniques for development and operation, I would love to write this article to share the experience which I have gotten when start developing my company project
This article contains a lot of not easily understandable techniques and terms. I hope you can enjoy it. You can find an example Nest project implementing all these concepts on GitHub.
Monolithic ---- The system consists of multiple services, but for whatever reason, it must be deployed as a whole.
Easy to develop and test.
Deploys as a single deployment unit.
Easily scale horizontally with the same deployment unit.
Requires less technical expertise and sharing the same underlying code.
Allows high performance by centralizing code and memory.
Suitable for small applications.
The complexity of a system increases with time.
New features take a long time to be released.
Production hotfixes take longer.
When a small change occurs in a module, the entire application has to be updated.
Unable to adopt newer technologies for better performance due to their close relationship with one technology.
Single point of failure.
Code becomes complex and doing new features becomes increasingly challenging due to high coupling.
High dependency on key developers who understand the entire code base
Continuous deployment is challenging.
Individual modules are difficult to scale.
The high coupling between modules causes reliability and availability issues.
Security concerns as all deployments are at one place.
Microservice ---- also known as microservice architecture ---- is an architectural style that structures an application as a collection of services that are
Highly maintainable and testable
Independently deployable and scalable
Adapt newer technology more easily
Resilience to failures
Accelerates the velocity of software development by enabling small, autonomous teams to work in parallel
Every coin has two sides, microservices also have disadvantages such as operation and management costs increasing when they become bigger. That's why Serverless was born to wipe out that cost.
Moreover, microservices have a symbiotic relationship with domain-driven design (DDD), which will be explained in the next section.
Domain-Driven Design (DDD) ---- is a design approach where the business domain is carefully modeled in software and evolved over time, independently of the plumbing that makes the system work. DDD is a key and necessary tool when designing microservices.
The business goal is important to the business users, with a clear interface and functions. This way, the microservice can run independently from other microservices. Moreover, the team can also work on it independently, which is, in fact, the point of the microservice architecture.
Eric Evans, introduced the concept in 2004, in his book Domain-Driven Design: Tackling Complexity in the Heart of Software, which focuses on three principles:
The primary focus of the project is the core domain and domain logic.
Complex designs are based on models of the domain.
Collaboration between technical and domain experts is crucial to creating an application model that will solve particular domain problems.
Serverless / AWS Lambda --- is an approach to software design that allows developers to build and run services without having to manage the underlying infrastructure. Developers can write and deploy code, while a cloud provider provisions servers to run their applications, databases, and storage systems at any scale.
Serverless architecture is best used to perform short-lived tasks and manage workloads that experience infrequent or unpredictable traffic
While serverless architecture has been around for more than a decade, Amazon introduced the first mainstream FaaS platform, AWS Lambda. If you have any concern about monitoring AWS Lambda, you can deep dive into this article
Since Serverless Architecture costs on-demand usage and runs easily without managing the infrastructure, it resolves microservice's disadvantages, which are reducing the management cost and operation cost.
Onion Architecture --- is an architectural pattern that enables maintainable and evolutionary enterprise systems to archive these goals:
Independent of Frameworks. The architecture does not depend on the existence of some library of feature-laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
Independent of any external agency. In fact, your business rules simply don’t know anything at all about the outside world.
User interface: a place for components designed to handle communication with a user by a specific channel; also provides the domain to the application (don’t confuse it with the UI on the front end) (in my case this one is API Controllers)
Infrastructure: Databases, Messaging systems, Notification systems, etc ...
Application services: the place for an application service/facade and, optionally, commands and queries (in my case this one is Services)
Domain services: repository interfaces and domain logic involving several entities (in my case this one is Repositories)
Domain model: the very center of the Model, this layer can have dependencies only on itself. It represents the Entities of the Business and the Behaviour of these Entities.
Onion Architecture is one of the specific applications of the concepts of Clean Architecture, but it's quite more simple, that's why I choose to apply.
NodeJS / NestJS ---- A framework for Node-based server-side applications, can be seen as Angular on the backend
You can use any programming language, like Java, C#, or Python to develop a microservice, but Node.js is an outstanding choice for a few reasons.
For one, Node.js uses an event-driven architecture and enables efficient, real-time application development. Node.js single-threading and asynchronous capabilities enable a non-blocking mechanism. Node.js can leverage plenty of superb NPM libraries. Developers using Node.js to build microservices have an uninterrupted flow, with Node.js code being fast, highly scalable, and easy to maintain.
While plenty of superb libraries, helpers, and tools exist for Node, Nest provides an out-of-the-box application architecture that allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications.
Many technologies are supported out of the box (GraphQL, Redis, Elasticsearch, TypeORM, microservices, CQRS…)
Built with Node.js and Supports both Express.js and Fastify
Dependency Injection built-in
According to a lot of advantages of NodeJs and NestJS listed above, and also my front-end project is mainly developed in Angular. I decided to choose them
AWS CDK ---- stands for AWS Cloud Development Kit. A framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation.
The AWS CDK lets you build reliable, scalable, cost-effective applications in the cloud with the considerable expressive power of a programming language. This approach yields many benefits, including:
Build with high-level constructs that automatically provide sensible, secure defaults for your AWS resources, defining more infrastructure with less code.
Use programming idioms like parameters, conditionals, loops, composition, and inheritance to model your system design from building blocks provided by AWS and others.
Put your infrastructure, application code, and configuration all in one place, ensuring that at every milestone you have a complete, cloud-deployable system.
Employ software engineering practices such as code reviews, unit tests, and source control to make your infrastructure more robust.
Connect your AWS resources together (even across stacks) and grant permissions using simple, intent-oriented APIs.
Import existing AWS CloudFormation templates to give your resources a CDK API.
Use the power of AWS CloudFormation to perform infrastructure deployments predictably and repeatedly, with rollback on error.
Easily share infrastructure design patterns among teams within your organization or even with the public.
I love Typescript, that's why I choose AWS CDK to do my Infrastructure. I treat AWS CDK Code same as the Business Logic Code and manage them like a normal library. Every microservice will have its own AWS CDK code to deploy independently.
From now, everything in my project will be developed in Typescript.
Nx ---- All in one smart, fast, and extensible build system with first-class monorepo support and powerful integrations.
Since we have a lot of things in our backend until now
DDD / Onion Architecture: Domain Core (Services, Repositories, Domain Models), Infrastructure, Utils, ....
Microservice / AWS Lambda Function
Thank God, Nx was born for managing them all
. └── src ├── apps │ ├── query-api <-- aws lambda (lib) │ │ ├── app <-- controllers (dir) │ │ └── cdk <-- aws cdk (dir) │ └── command-api <-- aws lambda (lib) │ │ ├── app <-- controllers (dir) │ │ └── cdk <-- aws cdk (dir) └── libs ├── domain <-- domain folder (dir) │ ├── core <-- core folder (dir) │ │ ├── domain <-- interfaces (lib) │ │ ├── repositories <-- interfaces (lib) │ │ └── services <-- interfaces (lib) │ ├── infrastructure <-- (lib) │ │ └── repositories <-- database access (dir) │ ├── services <-- (lib) │ │ ├── command <-- command service(dir) │ │ ├── query <-- query service(dir) │ │ └── models <-- request model (dir) │ └── utils <-- utilities (lib) ├── infra <-- infra group (dir) │ └── cdk <-- aws cdk (lib) ├── shared <-- shared libs group (dir) │ ├── database <-- shared database (lib) │ ├── environment <-- shared env (lib) │ └── utils <-- shared utils (lib)
Finally, everything will be deployed with the design below
In this infrastrure, I choose
AWS API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the "front door" for applications to access data, business logic, or functionality from your backend services
AWS DynamoDB is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-Region replication, in-memory caching, and data export tools.
Amazon Simple Queue Service (SQS) is a fully managed message queuing service that enables you to decouple and scale microservices, distributed systems, and serverless applications. SQS eliminates the complexity and overhead associated with managing and operating message-oriented middleware and empowers developers to focus on differentiating work.