How to implement a serverless architecture on Azure - Part 5

In this section, it's time to put theory into practice! We will establish the solutions to address the problems mentioned in the previous post.

Chacun son métier, les vaches seront bien gardées.
French proverb

Exploring Domain-Driven Design

Domain-Driven Design (DDD) is a software development methodology and a set of principles, patterns, and practices for designing and building complex software systems. DDD focuses on creating software that reflects real-world business domains and is driven by the core business concepts, rules, and requirements. The primary goal of DDD is to bridge the gap between the technical aspects of software development and the domain-specific knowledge of the subject matter experts (SMEs) or business stakeholders.

Key principles and concepts of Domain-Driven Design

  • DDD emphasizes the use of a common, shared vocabulary (ubiquitous langage) between technical and non-technical team members. This ensures that everyone involved in the project understands and uses the same terminology to describe the domain.
  • The domain is divided into bounded contexts, each representing a specific area with its own rules and models. These boundaries help in isolating domain knowledge and preventing conflicts.

Domain-Driven Design is particularly valuable when dealing with complex, large-scale software projects where a deep understanding of the business domain is essential. DDD encourages close collaboration between software developers and domain experts, leading to more effective and accurate software systems that align closely with the business's needs.

What are bounded contexts exactly ?

Good question !

Bounded contexts help in organizing and structuring a complex software system. A bounded context defines a specific, self-contained area within the overall domain of a software application where a particular model or set of models applies and holds its own distinct understanding of the domain.

Key characteristics and principles of bounded contexts

  • Each bounded context is isolated and independent, meaning it operates within its own set of rules, terminology, and models. It can have its own domain experts and development teams.
  • Inside a bounded context, there is a shared, ubiquitous language specific to that context. This means that the terms and language used within the context are consistent and meaningful to the stakeholders within that boundary.
  • Bounded contexts can have their own models and representations of domain concepts. These models are optimized for the specific needs of that context.
  • The boundaries of a bounded context are clearly defined, and the context is responsible for maintaining the consistency and integrity of the domain within those boundaries.
  • Where different bounded contexts interact, integration points are established. These points allow for communication and data exchange between contexts while respecting their autonomy.
  • A bounded context is responsible for ensuring that its models accurately represent the relevant domain concepts and that it enforces the domain rules and invariants within its boundaries.

Bounded contexts are particularly valuable in large and complex software systems where multiple teams are working on different parts of the application, and where the domain itself may have different interpretations or requirements in various areas. By clearly defining bounded contexts, DDD helps prevent misunderstandings, inconsistencies, and conflicts in the understanding of the domain.

For example, in an e-commerce application, you might have separate bounded contexts for order management, inventory control, and customer profiles. Each of these contexts has its own models, terminology, and rules, allowing the teams responsible for each context to work more autonomously and effectively. Integration between contexts is carefully managed to ensure data consistency and effective communication. In our toy example, we will have a bounded context for marketing, another one for authoring, another one for delivering and so on.

Bounded contexts facilitate a more systematic and structured approach to designing and developing complex software, promoting a better alignment between the software's design and the business domain it serves.

What does this have to do with our problem at hand ?

Bounded contexts will allow us to distinctly segregate our domain and establish multiple data models. In practice, each bounded context will be associated with a single function in our cloud platform (for example, in an Azure function for Azure).

Focus on Azure Functions

Azure Functions is a serverless compute service provided by Microsoft Azure. It allows developers to build and run event-driven, serverless applications without the need to manage infrastructure. Azure Functions is a part of the Azure serverless computing platform and is designed for microservices, integration, data processing, and automation tasks.

  • Azure Functions are triggered by various events, such as HTTP requests, messages in queues, changes in Azure Blob Storage, database updates, and timer-based schedules. They respond to events in near real-time.
  • With Azure Functions, we only pay for the compute resources used during the execution of our functions. There are no upfront costs, and we are billed based on the number of executions and execution time.
  • Azure Functions automatically scale based on the number of incoming requests or events. You don't need to worry about provisioning or managing infrastructure.
  • Azure Functions can easily integrate with other Azure services and external APIs, making them suitable for building serverless workflows and integrations.

Azure Functions are commonly used for a wide range of use cases, such as building REST APIs, processing data from IoT devices, automating routine tasks, creating webhooks, and implementing event-driven architectures. They provide a flexible and cost-effective way to run code in response to various events.

Azure function for authors

  • Define a blank solution in Visual Studio and name it EOCS.AuthoringManagement.

    Create a blank solution in Visual Studio

  • Create in this solution a new Azure Function project and name it EOCS.AuthoringManagement.Service.

    Create an Azure Function project

  • Add in this project a new folder named Data and, in this repository, add a new C# class and name it Book.cs. Define in it only the fields necessary for an author.

1public class Book
2{
3    public string ISBN { get; set; }
4    public string Title { get; set; }
5    public int PageCount { get; set; }
6    public string SubjectMatter { get; set; }
7    public List<string> Categories { get; set; }
8    public DateTime Deadline { get; set; }
9}
  • Add a new folder named Repositories and, in this repository, add a repository Interfaces. Add in this latter folder, add an interface named IBookRepository.
1public interface IBookRepository
2{
3    Task<List<Book>> GetAllBooks();
4
5    Task<Book> GetBookByISBN(int isbn);
6
7    Task SaveBook(Book book);
8}
  • Add in the Repositories folder a new class named SqlServerBookRepository.cs.
 1public class SqlServerBookRepository : IBookRepository
 2{
 3    public async List<Book> GetAllBooks()
 4    {
 5        var sql = "select * from Books";
 6
 7        // ...
 8    }
 9
10    public async Book GetBookByISBN(int isbn)
11    {
12        var sql = "select * from Books";
13    }
14
15    public async Task SaveBook(Book book)
16    {
17        throw new NotImplementedException();
18    }
19}
Important

It is only for convenience that we are using SqlServer, but of course, we could retrieve the data from any other datastore (like CosmosDb for example). In this particular context, we need to envision that there is a SqlServer database in the background dedicated to authors, and a "Books" table has been created in it. This table exclusively contains pertinent fields for an author and nothing more.

  • Add a StartUp.cs file at the root of the project.
 1[assembly: WebJobsStartup(typeof(StartUp))]
 2namespace EOCS.AuthoringManagement.Service
 3{
 4    public class StartUp : FunctionsStartup
 5    {
 6        public override void Configure(IFunctionsHostBuilder builder)
 7        {
 8            ConfigureServices(builder.Services);
 9        }
10
11        private static void ConfigureServices(IServiceCollection services)
12        {
13            services.AddSingleton<IBookRepository, SqlServerBookRepository>();
14        }
15    }
16}

Please note that this class will require additional NuGet packages.

This StartUp.cs file refers to a special file that contains the code and configuration necessary to bootstrap and configure the Azure Functions runtime. This file is an entry point for our Azure Functions application, and it provides the necessary initialization settings for our functions.

In our case, we configure and register services or dependencies (IBookRepository) that our functions require by using the built-in dependency injection feature in Azure Functions. In a real-world example, we will also set up application settings and connection strings and perform any other necessary initialization or configuration for our application (logging for instance).

  • Add a AuthorService.cs file at the root of the project.
 1public class AuthorService
 2{
 3    private readonly IBookRepository _bookRepository;
 4
 5    public AuthorService(IBookRepository bookRepository)
 6    {
 7        _bookRepository = bookRepository;
 8    }
 9
10    [FunctionName(nameof(GetAllBooks))]
11    public async Task<IActionResult> GetAllBooks([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
12    {
13        var books = await _bookRepository.GetAllBooks();
14        return new OkObjectResult(books);
15    }
16
17    [FunctionName(nameof(GetBookByISBN))]
18    public async Task<IActionResult> GetBookByISBN([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
19    {
20        var isbn = Convert.ToInt32(req.Query["isbn"]);
21
22        var book = await _bookRepository.GetBookByISBN(isbn);
23        return new OkObjectResult(book);
24    }
25    
26    // ...
27}

This file enables us to enumerate the methods accessible to third parties. It's worth noting how we can specify the HTTP methods (e.g., GET, POST) for each function. Once this foundational code is in place, these methods can be invoked by a front-end application.

  • In the end, the project's structure should resemble the figure below.

We can of course add other projects in this solution (for example a test project for unit tests).

Azure function for transporters

We can replicate the same process for another business. The logic remains consistent, and below is the outcome for the delivery-focused bounded context.

  • Define a new solution in Visual Studio and name it EOCS.DeliveringManagement.

  • Create in this solution a new Azure Function project and name it EOCS.DeliveringManagement.Service.

  • Add the necessary items for the bounded context (in particular the Book class).

1public class Book
2{
3    public string ISBN { get; set; }
4    public double Weight { get; set; }
5    public double Height { get; set; }
6    public double Width { get; set; }
7}

Azure Function for the SEO's professional

  • Define a new solution in Visual Studio and name it EOCS.SEOWriting.

  • Create in this solution a new Azure Function project and name it EOCS.SEOWriting.Service.

  • Add the necessary items for the bounded context (in particular the Book class).

1public class Book
2{
3    public string ISBN { get; set; }
4    public string Title { get; set; }
5    public List<string> Keywords { get; set; }
6    public List<string> Metadata { get; set; }
7    
8    // ...
9}

These projects are entirely self-contained, which makes developers less apprehensive about making changes. In any case, each developer can work at their own pace, and deployment schedules can vary for each Azure Function, aligning with the needs of each bounded context.

Minute, what happens if the book's title is edited ?

Imagine a scenario where an author changes the title of their book. Since this change is isolated within the bounded context, it won't be immediately visible to third parties, which is not a desirable outcome. Therefore, there needs to be a mechanism in place to notify other businesses that a change has occurred. This brings us into the intricate realm of inter-service communication.

On one hand, some parties may not be affected by this change because they are not interested in the book's title (this is the case for the Delivery bounded context for example). But what's the approach for the ones that are directly involved ? We are suggesting a standard and simplified solution here, based on asynchronous messaging. The concept is for the Authoring bounded context to dispatch a message to a service bus, to which other parties have subscribed.

  • Open the EOCS.AuthoringManagement solution, go the AuthoringService.cs service and add the following code.
 1[FunctionName(nameof(SaveBook))]
 2public async Task<IActionResult> SaveBook([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log)
 3{
 4    // Save book in the Authoring context
 5
 6    // Send a message to a queue
 7
 8    var request = new SaveBookRequest() { Name = "SAVE_BOOK_REQUEST", Book = NewBook }
 9
10    var queueClient = new QueueClient(StorageConnectionString, "books", new QueueClientOptions
11    {
12        MessageEncoding = QueueMessageEncoding.Base64
13    });
14    await queueClient.CreateIfNotExistsAsync();
15
16    var serialized = JsonConvert.SerializeObject(request);
17
18    await queueClient.SendMessageAsync(serialized);
19}

In this approach, we send a message that contains the serialized form of a book to an Azure queue names "books". In different scenarios, we could utilize Azure Service Bus or even Azure Event Grid.

  • Open the EOCS.SEOWriting solution, go the AuthoringService.cs service and add the following code.
1[FunctionName(nameof(SaveBookFromAuthoring))]
2public async Task SaveBookFromAuthoring([QueueTrigger("books", Connection = "QueueTriggersConnectionString")] string queueItem)
3{
4    var request = JsonConvert.DeserializeObject<SaveBookRequest>(queueItem);
5    var bookToUpdate = request.Book;
6
7    await _bookRepository.UpdateBook(bookToUpdate);
8}

We leverage the capability of an Azure Function to listen for a queue, allowing us to update the book. In this context, the trigger being used is a queue trigger, indicating that the method waits for an event from the queue.

We acknowledge that this approach is straightforward (although adequate in some situations) and that more sophisticated processes may be necessary. Nevertheless, it serves as a sufficient illustration to demonstrate how services can communicate with each other. We also acknowledge that certain intricate details have not been highlighted (for example, where the SaveBookRequest class come from and what to do if there are many subscribers to the queue ?). Once more, our aim is to elucidate the philosophy of serverless microservices rather than delve into the nitty-gritty particulars.

In the next part of this series, we will delve into the advantages offered by the microservices approach and also explore the challenges and complexities it entails. See you here