I will always find webhooks interesting. It's a meaty problem-space and there's a lot to it.
You build a mailbox (or a few) to receive messages, and you'll have to think about:
Node
TypeScript
SST - framework used to scaffold resources on AWS via infrastructure as code
ElectroDB - Single-Table design oriented framework for DynamoDB (WIP)
Golang for our send-webhook lambda function which is used to demo this application.
Method | Target | Notes |
---|---|---|
POST |
/api/webhooks/bigcommece |
Capture webhooks from BigCommerce |
POST |
/api/webhooks/stripe |
Capture webhooks from Stripe (not yet implemented) |
POST |
/demo/send-webhook |
Generates a BigCommerce Webhook event and sends it to our API |
Name | Notes |
---|---|
Failed Webhooks Queue | Rerun failed webhooks. Max two retries before messges get sent to the dead letter queue. |
Dead Webhooks Queue | Dead Letter Queue. This is where we can notify an operator. |
Name | Trigger | Description |
---|---|---|
capture-webhooks |
API Gateway Event | Capture webhooks and persist to DynamoDB |
dynamo-process-webhooks |
DynamoDB Stream Insert Event | Process webhook (first run) |
sqs-process-webhooks |
SQS Message from Failed Webhooks Queue | Process webhook (failed runs) |
These three lambdas build out the core functionality of the project.
Name | Trigger | Description |
---|---|---|
send-webhooks |
API Gateway Event | Creates and sends a BigCommerce webhook to our API. Built with Golang since different languages may marshall / unmarshall JSON data differently. |
These lambdas will are used to demo our features.
We will be using a Single-Table Design to store our webhooks data. Not too familar with this concept but this is a great opportunity to give this a go.
In a real world situation such as an eCommerce store, I would group webhooks and related data as their own collection of entities. And things like Customers, Orders, maybe Products in another multi-entity 'Single-Table'.
Also, I like the thesis of "Storage is cheap, compute isn't". I think this has some merit.
Name | PK | SK | Notes |
---|---|---|---|
Webhook | WH#<WH_ID> |
WEBHOOK |
Stores immutable information such as payload & metadata. Given the immutably, we can avoid additional reads and rely on this information being sent via DynamoDB Streams and SQS. |
Webhook Status | WH#<WH_ID> |
STATUS |
Stores mutable information such status and retries |
const webhook = {
PK: 'WH#57f08d25-a733-453a-be49-8caf04df5169',
SK: 'WEBHOOK',
id: '57f08d25-a733-453a-be49-8caf04df5169',
origin: 'stripe',
type: 'payment_intent.succeeded',
created: '2024-05-19T05:27:28+00:00',
payload: 'json payload',
}
const webhookStatus = {
PK: 'WH#57f08d25-a733-453a-be49-8caf04df5169',
SK: 'STATUS',
id: '57f08d25-a733-453a-be49-8caf04df5169'
status: 'completed',
retries: 1,
}
Note: schema is subject to change.
Access Patterns | Target | Parameters | Notes |
---|---|---|---|
Get Webhook | Main Table | - Webhook ID | Get Operation - Given the immutability, we can always use 'eventually' consistent reads |
Capture Webhook | Main Table | - Webhook ID | Transaction with Two Operations: - Put Webhook - Put Webhook Status |
Get Webhook Status | Main Table | - Webhook ID | Get Operation |
Set Webhook Status | Main Table | - Webhook ID | Update Operation |
Set Webhook to processing |
Main Table | - Webhook ID | Update Operation - set status to processing - increment retries |