wojciech-dabrowski
wojciech-dabrowski
wojc.dabrowski@gmail.com
there and back again
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);
}
}
}
}
Repetition mania
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)
{
_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)
{
_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);
}
}
Common closure principle application
Portable library
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
};
await s3Client.PutObjectAsync(s3PutRequest);
}
}
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);
}
}
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>();
}
public class LoanHasBeenPaidOffEventHandler : IEventHandler<LoanHasBeenPaidOffEvent>
{
...
private readonly IEmailClient _emailClient;
public LoanHasBeenPaidOffEventHandler(IEmailClient emailClient)
{
_emailClient = emailClient;
}
public void Handle(LoanHasBeenPaidOffEvent @event)
{
...
var insertMailModel = new InsertEmailModel
{
To = @event.CustomerMailAddress,
Subject = MailSubject,
Body = mailBody
};
_emailClient.InsertEmail(insertMailModel);
}
}
def insertEmail(insertEmailModel):
return requests.post(insertEmailUrl, data = insertEmailModel)
def insertEmail(insertEmailModel):
return requests.post(insertEmailUrl, data = insertEmailModel)
export class EmailService {
...
constructor(private http: HttpClient) {}
insertEmail(insertEmailModel: InsertEmailModel): Observable<string> {
return this.http.post<string>(this.insertEmailUrl, insertEmailModel);
}
}
Increased system complexity
Increased system complexity
Maintenance, service ownership in team or organization
Increased system complexity
Maintenance, service ownership in team or organization
Versioning
Increased system complexity
Maintenance, service ownership in team or organization
Versioning
Testing
Increased system complexity
Maintenance, service ownership in team or organization
Versioning
Testing
Deploying
Deploying
Deploy new version of the code (e.g. Lambda)
Deploy new version of the code (e.g. Lambda)
Update infrastructure
Deploy new version of the code (e.g. Lambda)
Update infrastructure
Create new environment from scratch
AWS
CloudFormation
https://www.dccomics.com/characters/superman
"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" }
}
}
}
}
}
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
aws cloudformation deploy --template-file serverless-mail.yaml --stack-name serverless-mail --region eu-west-1
https://giphy.com/gifs/machine-goldberg-rube-SFq1DyiaqOxK8
AWS CodePipeline
ArtifactStore:
Type: S3
Stages:
- Name: Source
Actions:
- Name: CodePackage
ActionTypeId:
Category: Source
Owner: AWS
Provider: S3
OutputArtifacts:
- Name: CodePackage
PollForSourceChanges: true
- Name: CodeConfiguration
ActionTypeId:
Category: Source
Owner: AWS
Provider: S3
OutputArtifacts:
- Name: CodeConfiguration
PollForSourceChanges: true
Stages:
...
- Name: CreateChangeSet
Actions:
- Name: CreateChangeSet
InputArtifacts:
- Name: CodePackage
- Name: CodeConfiguration
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Configuration:
ActionMode: CHANGE_SET_REPLACE
- Name: DeployChangeSet
Actions:
- Name: DeployChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Configuration:
ActionMode: CHANGE_SET_EXECUTE
Deploy new version of the code (e.g. Lambda)
Deploy new version of the code (e.g. Lambda)
Update infrastructure
Deploy new version of the code (e.g. Lambda)
Update infrastructure
Create new environment from scratch
Trivial example
Trivial example
Match the solution with requirements
Trivial example
wojciech-dabrowski
wojciech-dabrowski
wojc.dabrowski@gmail.com
Northmill
facebook.com/northmill.se
northmill.com
SpreadIT
facebook.com/SpreadITpl
spreadit.pl
Gruba IT
facebook.com/grubait
gruba.it