Tech With Htunn
  • Blog Content
  • ๐Ÿค–Artificial Intelligence
    • ๐Ÿง Building an Intelligent Agent with Local LLMs and Azure OpenAI
    • ๐Ÿ“ŠRevolutionizing IoT Monitoring: My Personal Journey with LLM-Powered Observability
  • ๐Ÿ“˜Core Concepts
    • ๐Ÿ”„Understanding DevSecOps
    • โฌ…๏ธShifting Left in DevSecOps
    • ๐Ÿ“ฆUnderstanding Containerization
    • โš™๏ธWhat is Site Reliability Engineering?
    • โฑ๏ธUnderstanding Toil in SRE
    • ๐Ÿ”What is Identity and Access Management?
    • ๐Ÿ“ŠMicrosoft Graph API: An Overview
    • ๐Ÿ”„Understanding Identity Brokers
  • ๐Ÿ”ŽSecurity Testing
    • ๐Ÿ”SAST vs DAST: Understanding the Differences
    • ๐ŸงฉSoftware Composition Analysis (SCA)
    • ๐Ÿ“‹Software Bill of Materials (SBOM)
    • ๐ŸงชDependency Scanning in DevSecOps
    • ๐ŸณContainer Scanning in DevSecOps
  • ๐Ÿ”„CI/CD Pipeline
    • ๐Ÿ”My Journey with Continuous Integration in DevOps
    • ๐Ÿš€My Journey with Continuous Delivery and Deployment in DevOps
  • ๐ŸงฎFundamentals
    • ๐Ÿ’พWhat is Data Engineering?
    • ๐Ÿ”„Understanding DataOps
    • ๐Ÿ‘ทThe Role of a Cloud Architect
    • ๐Ÿ›๏ธCloud Native Architecture
    • ๐Ÿ’ปCloud Native Applications
  • ๐Ÿ›๏ธArchitecture & Patterns
    • ๐Ÿ…Medallion Architecture in Data Engineering
    • ๐Ÿ”„ETL vs ELT Pipeline: Understanding the Differences
  • ๐Ÿ”’Authentication & Authorization
    • ๐Ÿ”‘OAuth 2.0 vs OIDC: Key Differences
    • ๐Ÿ”Understanding PKCE in OAuth 2.0
    • ๐Ÿ”„Service Provider vs Identity Provider Initiated SAML Flows
  • ๐Ÿ“‹Provisioning Standards
    • ๐Ÿ“ŠSCIM in Identity and Access Management
    • ๐Ÿ“กUnderstanding SCIM Streaming
  • ๐Ÿ—๏ธDesign Patterns
    • โšกEvent-Driven Architecture
    • ๐Ÿ”’Web Application Firewalls
  • ๐Ÿ“ŠReliability Metrics
    • ๐Ÿ’ฐError Budgets in SRE
    • ๐Ÿ“SLA vs SLO vs SLI: Understanding the Differences
    • โฑ๏ธMean Time to Recovery (MTTR)
Powered by GitBook
On this page
  • What is Event-Driven Architecture? My Perspective
  • The Key Elements I've Learned to Work With
  • My Real-World Implementation: Building a Customer Processing Pipeline
  • System Overview: The Business Problem
  • Step 1: Setting Up My AWS Infrastructure
  • Step 2: Developing the Event Producer Lambda (Registration API)
  • Step 3: Building the Customer Validation Service
  • Step 4: Creating the Account Generation Service
  • Step 5: Creating the Welcome Email Service
  • Step 6: Deploying with Infrastructure as Code
  • What I've Learned: The Real Benefits of Event-Driven Architecture
  • 1. Resilience Through Decoupling
  • 2. Scalability Through Asynchronous Processing
  • 3. System Evolution and Extensibility
  • 4. Challenges I've Had to Overcome
  • Conclusion
  1. Design Patterns

Event-Driven Architecture

I still remember the day when I hit the scalability wall with our monolithic application. The system was crashing under load, components were tightly coupled, and adding new features felt like performing surgery. That's when I discovered event-driven architectureโ€”a paradigm shift that transformed how I design and build distributed systems.

What is Event-Driven Architecture? My Perspective

Event-driven architecture (EDA) isn't just another technical buzzwordโ€”it's a fundamentally different way of thinking about system interactions. In my experience, it's like moving from direct phone calls to a sophisticated postal system where messages can be reliably delivered, persisted, and processed at the receiver's pace.

At its core, EDA is built around a simple concept: systems communicate through eventsโ€”facts that have happenedโ€”rather than direct commands. After implementing several production systems this way, I've come to appreciate how this subtle shift creates remarkably resilient and scalable applications.

The Key Elements I've Learned to Work With

After several years of building event-driven systems, I've identified these critical components:

  1. Events - These are immutable records of something that happened. I treat these as sacred facts that describe state changes in the system.

  2. Event Producers - Components in my systems that detect state changes and publish events without caring who (if anyone) is listening.

  3. Event Consumers - Services that subscribe to events and react accordingly, completely decoupled from the producers.

  4. Event Brokers - The messaging infrastructure (like AWS SQS and EventBridge) that ensures reliable delivery between producers and consumers.

My Real-World Implementation: Building a Customer Processing Pipeline

Let me share a practical example of how I built an event-driven customer data processing pipeline using Python microservices deployed on AWS Lambda with SQS and EventBridge.

System Overview: The Business Problem

My team needed to build a system that could:

  • Register new customers

  • Validate their information

  • Create accounts in multiple downstream systems

  • Send welcome communications

  • Generate analytics

Instead of building a monolithic API that could fail at any step, I designed an event-driven workflow where each step was its own microservice, communicating through events.

Step 1: Setting Up My AWS Infrastructure

First, I established the messaging backbone using AWS services:

# Create an SQS queue for customer registration events
aws sqs create-queue --queue-name customer-registration-queue

# Create an EventBridge rule to capture account creations
aws events put-rule \
    --name "CustomerAccountCreatedRule" \
    --event-pattern '{"source":["custom.customerService"],"detail-type":["CustomerAccountCreated"]}'

Step 2: Developing the Event Producer Lambda (Registration API)

I created a simple API endpoint using API Gateway and Lambda that would register customers and publish events:

# customer_registration_lambda.py
import json
import boto3
import uuid
import datetime

sqs = boto3.client('sqs')
QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789012/customer-registration-queue'

def lambda_handler(event, context):
    # Extract customer data from API request
    request_body = json.loads(event['body'])
    
    # Generate a unique customer ID
    customer_id = str(uuid.uuid4())
    
    # Create the event payload
    customer_event = {
        'eventType': 'CustomerRegistered',
        'timestamp': datetime.datetime.now().isoformat(),
        'customerId': customer_id,
        'data': {
            'firstName': request_body['firstName'],
            'lastName': request_body['lastName'],
            'email': request_body['email'],
            'phone': request_body.get('phone', ''),
            'address': request_body.get('address', {})
        }
    }
    
    # Send the event to SQS
    response = sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=json.dumps(customer_event)
    )
    
    return {
        'statusCode': 202,
        'headers': {'Content-Type': 'application/json'},
        'body': json.dumps({
            'message': 'Customer registration initiated',
            'customerId': customer_id,
            'trackingId': response['MessageId']
        })
    }

I learned early on to return a 202 Accepted status rather than 200 OK, signaling that the request was accepted but processing continues asynchronouslyโ€”setting the right expectations with API consumers.

Step 3: Building the Customer Validation Service

Next, I created a Lambda function that processes registration events from SQS:

# customer_validation_lambda.py
import json
import boto3
import re

events = boto3.client('events')

def is_valid_email(email):
    # Simple validation - in production I'd use a more robust solution
    pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
    return bool(re.match(pattern, email))

def lambda_handler(event, context):
    for record in event['Records']:
        # Parse the message
        message_body = json.loads(record['body'])
        customer_data = message_body['data']
        
        # Validate customer data
        is_valid = True
        validation_errors = []
        
        if not is_valid_email(customer_data['email']):
            is_valid = False
            validation_errors.append('Invalid email format')
        
        if len(customer_data['firstName']) < 2:
            is_valid = False
            validation_errors.append('First name too short')
            
        # Determine event outcome based on validation
        if is_valid:
            outcome_event = {
                'Source': 'custom.customerService',
                'DetailType': 'CustomerValidated',
                'Detail': json.dumps({
                    'customerId': message_body['customerId'],
                    'status': 'VALID',
                    'customerData': customer_data
                }),
                'EventBusName': 'default'
            }
        else:
            outcome_event = {
                'Source': 'custom.customerService',
                'DetailType': 'CustomerValidationFailed',
                'Detail': json.dumps({
                    'customerId': message_body['customerId'],
                    'status': 'INVALID',
                    'errors': validation_errors,
                    'customerData': customer_data
                }),
                'EventBusName': 'default'
            }
        
        # Publish the outcome event to EventBridge
        events.put_events(Entries=[outcome_event])
        
    return {'statusCode': 200}

This microservice has a single responsibility: validate customer data and publish appropriate events based on the outcome. It doesn't need to know what happens next!

Step 4: Creating the Account Generation Service

When a customer is validated, another Lambda function picks up that event from EventBridge and creates the account:

# account_creation_lambda.py
import json
import boto3
import os
import uuid

events = boto3.client('events')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['CUSTOMER_TABLE_NAME'])

def lambda_handler(event, context):
    # Extract the event detail
    detail = json.loads(event['detail'])
    customer_id = detail['customerId']
    customer_data = detail['customerData']
    
    # Generate account credentials
    account_id = str(uuid.uuid4())
    default_password = f"Welcome{uuid.uuid4().hex[:8]}"
    
    # Store customer in database
    table.put_item(
        Item={
            'customerId': customer_id,
            'accountId': account_id,
            'email': customer_data['email'],
            'firstName': customer_data['firstName'],
            'lastName': customer_data['lastName'],
            'status': 'ACTIVE',
            'createdAt': event['time']
        }
    )
    
    # Publish account created event
    account_created_event = {
        'Source': 'custom.customerService',
        'DetailType': 'CustomerAccountCreated',
        'Detail': json.dumps({
            'customerId': customer_id,
            'accountId': account_id,
            'email': customer_data['email'],
            'name': f"{customer_data['firstName']} {customer_data['lastName']}"
        }),
        'EventBusName': 'default'
    }
    
    events.put_events(Entries=[account_created_event])
    
    return {'statusCode': 200}

Step 5: Creating the Welcome Email Service

Finally, I built a notification service that sends welcome emails when accounts are created:

# welcome_email_lambda.py
import json
import boto3
import os

ses = boto3.client('ses')
SENDER_EMAIL = os.environ['SENDER_EMAIL']

def lambda_handler(event, context):
    # Parse event detail
    detail = json.loads(event['detail'])
    
    # Prepare email content
    recipient = detail['email']
    name = detail['name']
    account_id = detail['accountId']
    
    subject = f"Welcome to Our Service, {name}!"
    body_html = f"""
    <html>
    <body>
        <h1>Welcome aboard, {name}!</h1>
        <p>Your account has been created successfully.</p>
        <p>Your account ID is: <strong>{account_id}</strong></p>
        <p>Please log in to set up your profile and explore our services.</p>
    </body>
    </html>
    """
    
    # Send email
    try:
        response = ses.send_email(
            Source=SENDER_EMAIL,
            Destination={'ToAddresses': [recipient]},
            Message={
                'Subject': {'Data': subject},
                'Body': {'Html': {'Data': body_html}}
            }
        )
        return {'statusCode': 200, 'emailId': response['MessageId']}
    except Exception as e:
        print(f"Error sending email: {str(e)}")
        return {'statusCode': 500, 'error': str(e)}

Step 6: Deploying with Infrastructure as Code

I use the Serverless Framework to deploy all these components with proper configuration:

# serverless.yml
service: event-driven-customer-service

provider:
  name: aws
  runtime: python3.9
  region: us-east-1
  environment:
    CUSTOMER_TABLE_NAME: ${self:service}-customers-${self:provider.stage}
    SENDER_EMAIL: "no-reply@mycompany.com"
  iamRoleStatements:
    - Effect: Allow
      Action:
        - sqs:SendMessage
        - sqs:ReceiveMessage
        - sqs:DeleteMessage
        - events:PutEvents
        - dynamodb:PutItem
        - ses:SendEmail
      Resource: "*"  # In production, I'd scope these down further

functions:
  customerRegistration:
    handler: customer_registration_lambda.lambda_handler
    events:
      - http:
          path: api/customers
          method: post
          cors: true
  
  customerValidation:
    handler: customer_validation_lambda.lambda_handler
    events:
      - sqs:
          arn:
            Fn::GetAtt: [CustomerRegistrationQueue, Arn]
  
  accountCreation:
    handler: account_creation_lambda.lambda_handler
    events:
      - eventBridge:
          pattern:
            source:
              - custom.customerService
            detail-type:
              - CustomerValidated
  
  welcomeEmail:
    handler: welcome_email_lambda.lambda_handler
    events:
      - eventBridge:
          pattern:
            source:
              - custom.customerService
            detail-type:
              - CustomerAccountCreated

resources:
  Resources:
    CustomerRegistrationQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-customer-registration-${self:provider.stage}
        VisibilityTimeout: 60
    
    CustomerTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-customers-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: customerId
            AttributeType: S
        KeySchema:
          - AttributeName: customerId
            KeyType: HASH

What I've Learned: The Real Benefits of Event-Driven Architecture

After implementing several systems using this approach, here are my key takeaways:

1. Resilience Through Decoupling

One of the biggest advantages I've seen is how service failures stay isolated. In my previous monolithic systems, a single component failing would bring down the entire application.

With my event-driven architecture, if the email service goes down, customers can still register and accounts get createdโ€”the emails will be sent when the service recovers. This resilience has dramatically improved our system uptime.

2. Scalability Through Asynchronous Processing

The ability to scale services independently has been a game-changer. During marketing campaigns when registrations spike, my SQS queues absorb the traffic bursts, allowing the validation services to process events at their own pace without being overwhelmed.

I've been able to optimize the cost-performance ratio of each component independently:

  • Registration API scales to handle concurrent connections

  • Validation service scales based on SQS queue depth

  • Account creation and email services scale based on event volume

3. System Evolution and Extensibility

Adding new functionality without disturbing existing components is where event-driven architecture truly shines. Recently, I needed to add fraud detection to the customer registration flow. Instead of modifying any existing code, I simply created a new Lambda function subscribed to the CustomerValidated event.

This pattern has allowed our system to evolve organically as business requirements change.

4. Challenges I've Had to Overcome

It's not all smooth sailing. Here are some challenges I've faced:

  • Debugging can be harder: Tracing issues across distributed, asynchronous events requires good observability tools. I've had to invest in proper logging and monitoring.

  • Eventual consistency: Team members had to adjust to the fact that operations weren't immediately reflected across the system.

  • Event schema evolution: Changing event structures requires careful planning to maintain backward compatibility.

  • Local development complexity: Testing the entire system locally was challenging. I built a Docker-based local environment that emulates AWS services.

Conclusion

Event-driven architecture with Python and AWS managed services has fundamentally changed how I build systems. The shift from synchronous, tightly coupled components to asynchronous, event-driven microservices has improved system resilience, scalability, and maintainability in ways I never thought possible.

If you're dealing with complex workflows or systems that need to scale independently, I highly recommend exploring this approach. The initial learning curve is steep, but the long-term benefits to your system architecture and team velocity are substantial.

Remember, events don't just describe what happenedโ€”they create opportunities for your system to evolve in ways you might not have anticipated when you first designed it.

PreviousUnderstanding SCIM StreamingNextWeb Application Firewalls

Last updated 22 hours ago

๐Ÿ—๏ธ
โšก