wojciech-dabrowski

wojciech-dabrowski

wojc.dabrowski@gmail.com

Serverless Microservice

there and back again

Once upon a time...

public class CustomerHasTakenLoanEventHandler : IEventHandler<CustomerHasTakenLoanEvent>
{
    ...

    public void Handle(CustomerHasTakenLoanEvent @event)
    {
        ...

        var mailBody = $"Hi, {@event.CustomerFirstName}\n\n" +
                       $"You have taken a loan for {@event.LoanAmount} {@event.LoanCurrency}.";

        SendMail(@event.CustomerMailAddress, mailBody);
    }

    private void SendMail(string toMailAddress, string mailBody)
    {
        using (var emailMessage = new MailMessage())
        {
            emailMessage.To.Add(toMailAddress);

            emailMessage.From = mailConfig.FromMailAddress;
            emailMessage.Subject = MailSubject;
            emailMessage.Body = mailBody;
            emailMessage.IsBodyHtml = true;
            emailMessage.BodyEncoding = Encoding.Unicode;

            using (var smtpClient = new SmtpClient(smtpConfig.SmtpServerHost))
            {
                smtpClient.Port = smtpConfig.SmtpPort;
                smtpClient.UseDefaultCredentials = false;
                smtpClient.Credentials = new NetworkCredential(smtpConfig.SmtpUserName, smtpConfig.SmtpUserPassword);

                smtpClient.Send(emailMessage);
            }
        }
    }
}
public class LoanHasBeenPaidOffEventHandler : IEventHandler<LoanHasBeenPaidOffEvent>
{
    ...

    public void Handle(LoanHasBeenPaidOffEvent @event)
    {
        ...

        var mailBody = $"Hi, {@event.CustomerFirstName}\n\n" +
                       $"Your loan for {@event.LoanAmount} {@event.LoanCurrency} has been paid off.";

        SendMail(@event.CustomerMailAddress, mailBody);
    }

    private void SendMail(string toMailAddress, string mailBody)
    {
        using (var emailMessage = new MailMessage())
        {
            emailMessage.To.Add(toMailAddress);

            emailMessage.From = mailConfig.FromMailAddress;
            emailMessage.Subject = MailSubject;
            emailMessage.Body = mailBody;
            emailMessage.IsBodyHtml = true;
            emailMessage.BodyEncoding = Encoding.Unicode;

            using (var smtpClient = new SmtpClient(smtpConfig.SmtpServerHost))
            {
                smtpClient.Port = smtpConfig.SmtpPort;
                smtpClient.UseDefaultCredentials = false;
                smtpClient.Credentials = new NetworkCredential(smtpConfig.SmtpUserName, smtpConfig.SmtpUserPassword);

                smtpClient.Send(emailMessage);
            }
        }
    }
}

Approach #1

Repetition mania

Cons

Cons

  • Increases codebase which has to be maintained.

Cons

  • Increases codebase which has to be maintained.
  • Lack of testability.

https://giant.gfycat.com/TautWarmheartedBetafish.webm

Approach #2

Single responsibility principle application

public class SmtpClientEmailSender : IEmailSender
{
    ...

    public void SendMail(SendEmailModel model)
    {
        using (var emailMessage = new MailMessage())
        {
            emailMessage.To.Add(model.ToMailAddress);

            emailMessage.From = mailConfig.FromMailAddress;
            emailMessage.Subject = model.MailSubject;
            emailMessage.Body = model.MailBody;
            emailMessage.IsBodyHtml = true;
            emailMessage.BodyEncoding = Encoding.Unicode;

            using (var smtpClient = new SmtpClient(smtpConfig.SmtpServerHost))
            {
                smtpClient.Port = smtpConfig.SmtpPort;
                smtpClient.UseDefaultCredentials = false;
                smtpClient.Credentials = new NetworkCredential(smtpConfig.SmtpUserName, smtpConfig.SmtpUserPassword);

                smtpClient.Send(emailMessage);
            }
        }
    }
}
public class CustomerHasTakenLoanEventHandler : IEventHandler<CustomerHasTakenLoanEvent>
{
    ...
    private readonly IEmailSender emailSender;    

    public CustomerHasTakenLoanEventHandler(IEmailSender emailSender)
    {
        this.emailSender = emailSender;
    }

    public void Handle(CustomerHasTakenLoanEvent @event)
    {
        ...

        var mailBody = $"Hi, {@event.CustomerFirstName}\n\n" +
                       $"You have taken a loan for {@event.LoanAmount} {@event.LoanCurrency}.";
        var sendEmailModel = new SendEmailModel(@event.CustomerMailAddress, MailSubject, mailBody);

        emailSender.SendMail(sendEmailModel);
    }
}
public class LoanHasBeenPaidOffEventHandler : IEventHandler<LoanHasBeenPaidOffEvent>
{
    ...
    private readonly IEmailSender emailSender;

    public LoanHasBeenPaidOffEventHandler(IEmailSender emailSender)
    {
        this.emailSender = emailSender;
    }

    public void Handle(LoanHasBeenPaidOffEvent @event)
    {
        ...

        var mailBody = $"Hi, {@event.CustomerFirstName}\n\n" +
                       $"Your loan for {@event.LoanAmount} {@event.LoanCurrency} has been paid off.";
        var sendEmailModel = new SendEmailModel(@event.CustomerMailAddress, MailSubject, mailBody);

        emailSender.SendMail(sendEmailModel);
    }
}

PROS

PROS

  • Reuse in current module/context.

PROS

  • Reuse in current module/context.
  • Testability.

PROS

  • Reuse in current module/context.
  • Testability.
  • Better maintenance.

What if...

Cons

  • No possibility to use this functionality outside current module/context.

Approach #3

Common closure principle application

PROS

PROS

  • Reuse in current logical container
    (for example solution in .NET).

What if...

Cons

  • No possibility to use this functionality outside current logical container. For example new project/product.

Approach #4

Portable library

What if...

Cons

  • No possibility to use this package in other technology.

Cons

  • No possibility to use this package in other technology.
  • Scalability?

Cons

  • No possibility to use this package in other technology.
  • Scalability?
  • Availability?

Approach #5

One service to serve them all

API Gateway

API Gateway

Lambda

API Gateway

Lambda

SQS

API Gateway

Lambda

SQS

CloudWatch

API Gateway

Lambda

SQS

CloudWatch

SES

API Gateway

Lambda

SQS

CloudWatch

SES

S3

API Gateway

Lambda

SQS

CloudWatch

SES

S3

public class SmtpClientEmailSender : IEmailSender
{
    ...

    public void SendMail(SendEmailModel model)
    {
        using (var emailMessage = new MailMessage())
        {
            emailMessage.To.Add(model.ToMailAddress);

            emailMessage.From = mailConfig.FromMailAddress;
            emailMessage.Subject = model.MailSubject;
            emailMessage.Body = model.MailBody;
            emailMessage.IsBodyHtml = true;
            emailMessage.BodyEncoding = Encoding.Unicode;

            using (var smtpClient = new SmtpClient(smtpConfig.SmtpServerHost))
            {
                smtpClient.Port = smtpConfig.SmtpPort;
                smtpClient.UseDefaultCredentials = false;
                smtpClient.Credentials = new NetworkCredential(smtpConfig.SmtpUserName, smtpConfig.SmtpUserPassword);

                smtpClient.Send(emailMessage);
            }
        }
    }
}
using (var smtpClient = new SmtpClient(smtpConfig.SmtpServerHost))
{
    ...

    smtpClient.Send(emailMessage);
}
public async Task<string> InsertEmailAsync(InsertEmailModel emailModel, ILambdaContext context)
{
    var emailStorageId = Guid.NewGuid().ToString();

    await UploadEmailToS3(emailStorageId, emailModel);

    await sqsClient.SendMessageAsync(
        EnvironmentVariables.MailQueue,
        JsonConvert.SerializeObject(emailStorageId)
    );

    return emailStorageId;
}
private async Task UploadEmailToS3(string emailStorageId, InsertEmailModel emailModel)
{
    using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(emailModel))))
    {
        var s3PutRequest = new PutObjectRequest
        {
            BucketName = EnvironmentVariables.MailSendingBucket,
            Key = emailStorageId,
            InputStream = stream
        };

        var result = await s3Client.PutObjectAsync(s3PutRequest);

        if (result.HttpStatusCode != HttpStatusCode.OK)
        {
            throw new Exception($"An error occured during putting S3 object."
                              + $"Received status code {(int) result.HttpStatusCode} in response");
        }
    }
}
private async Task PollQueueAsync(ILambdaContext context)
{
    var request = new ReceiveMessageRequest
    {
        QueueUrl = EnvironmentVariables.MailQueue,
        MaxNumberOfMessages = EnvironmentVariables.MailNumberPerBatch
    };

    var sqsResult = await sqsClient.ReceiveMessageAsync(request);

    var sendEmailTasks = sqsResult.Messages
                                  .Select(async sqsMessage => await InvokeLambdaAndDeleteMessage(sqsMessage));

    await Task.WhenAll(sendEmailTasks);
}
private async Task InvokeLambdaAndDeleteMessage(Message sqsMessage)
{
    var request = new InvokeRequest
    {
        FunctionName = EnvironmentVariables.SendEmailLambdaName,
        Payload = sqsMessage.Body
    };

    var invokeResponse = await lambdaClient.InvokeAsync(request);

    if (WasCorrectlySent(invokeResponse))
    {
        await DeleteMessage(sqsMessage, invokeResponse);
    }
}
private async Task DeleteMessage(Message sqsMessage)
{
    var deleteMessageRequest = new DeleteMessageRequest
    {
        QueueUrl = EnvironmentVariables.MailQueue,
        ReceiptHandle = sqsMessage.ReceiptHandle
    };

    await sqsClient.DeleteMessageAsync(deleteMessageRequest);
}
public async Task<bool> SendEmailAsync(string emailTaskId, ILambdaContext context)
{
    var insertEmailModel = await GetEmailObjectFromS3(emailTaskId);

    var sendEmailRequest = CreateRawEmailModel(insertEmailModel);

    var result = await sesClient.SendRawEmailAsync(sendEmailRequest);

    return result.HttpStatusCode == HttpStatusCode.OK;
}
private async Task<InsertEmailModel> GetEmailObjectFromS3(string emailTaskId)
{
    var s3Object = await s3Client.GetObjectAsync(EnvironmentVariables.MailSendingBucket, emailTaskId);

    using (var reader = new StreamReader(s3Object.ResponseStream))
    {
        var s3Contents = reader.ReadToEnd();

        return JsonConvert.DeserializeObject<InsertEmailModel>(s3Contents);
    }
}

How to use it?

public async Task<string> InsertEmailAsync(InsertEmailModel insertEmailModelModel)
{
    var lambdaRequest = new InvokeRequest
    {
        FunctionName = insertEmailLambdaName,
        Payload = JsonConvert.SerializeObject(insertEmailModelModel)
    };

    var invokeResult = await lambdaClient.Value.InvokeAsync(lambdaRequest);

    CheckLambdaResult(invokeResult);

    return invokeResult.Payload.Deserialize<string>();
}
"Resources" : {

    "MailSendingBucket": {
        "Type": "AWS::S3::Bucket"
    },

    "InsertEmail" : {
      "Type" : "AWS::Serverless::Function",
      "Properties": {
        "Handler": "ServerlessMicroservice::ServerlessMicroservice.Lambdas.InsertEmailLambda::InsertEmailAsync",
        "Environment" : {
          "Variables" : {
            "MailQueueUrl" : { "Ref" : "MailQueue" },
            "MailSendingBucket" : { "Ref" : "MailSendingBucket" },
          }
        }
      }
    },

    "SendEmail" : {
      "Type" : "AWS::Serverless::Function",
      "Properties": {
        "Handler": "ServerlessMicroservice::ServerlessMicroservice.Lambdas.SendEmailLambda::SendEmailAsync",
        "Environment" : {
          "Variables" : {
            "MailQueueUrl" : { "Ref" : "MailQueue" },
            "MailSendingBucket" : { "Ref" : "MailSendingBucket" },
          }
        }
      }
    },

Challenges

Challenges

  • Maintenance, service ownership in team or organization.

Challenges

  • Maintenance, service ownership in team or organization.
  • Versioning.

Challenges

  • Maintenance, service ownership in team or organization.
  • Versioning.
  • Debugging, logging, testing.

Challenges

  • Maintenance, service ownership in team or organization.
  • Versioning.
  • Debugging, logging, testing.
  • Latency.

FINAL THOUGHTS

FINAL THOUGHTS

FINAL THOUGHTS

Match the solution with requirements

wojciech-dabrowski

wojciech-dabrowski

wojc.dabrowski@gmail.com

Northmill

facebook.com/northmill.se

northmill.com

SpreadIT

facebook.com/SpreadITpl

spreadit.pl

Q&A

Serverless microservice - there and back again

By Wojciech Dąbrowski

Serverless microservice - there and back again

  • 284