Key Takeaways
- An
aws payment gatewayarchitecture should keep raw card data out of your infrastructure entirely by using a PCI-compliant tokenization provider (Stripe, Braintree, Adyen) for the actual card handling. - Reducing PCI DSS scope is an architecture decision, not a paperwork exercise — the fewer systems that touch cardholder data, the smaller your audit (SAQ-A vs. SAQ-D is a massive difference in effort).
- Idempotency keys are non-negotiable for payment APIs — network retries must never double-charge a customer.
- Event-driven processing (SQS/SNS/EventBridge) decouples the payment confirmation from downstream fulfillment, inventory, and notification systems so a slow downstream service can't block checkout.
- Every payment-related write needs an audit trail: CloudTrail for infrastructure actions, and your own append-only ledger table for business-level payment state changes.
The riskiest part of "building a payment system on AWS" is usually the part teams try to build themselves: storing and handling raw card numbers. The right AWS architecture doesn't do that at all — it uses a PCI-certified payment processor for tokenization and keeps AWS responsible for orchestration, security, and scale around that boundary.
Reference Architecture
<svg viewBox="0 0 680 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="AWS payment processing architecture with API Gateway, Lambda, payment processor, and event fan-out">
<style>
.box { fill: none; stroke: currentColor; stroke-width: 1.5; }
.lbl { fill: currentColor; font-family: sans-serif; font-size: 12px; }
.arrow { stroke: currentColor; stroke-width: 1.5; marker-end: url(#a3); fill: none; }
</style>
<defs>
<marker id="a3" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
<path d="M0,0 L8,4 L0,8 Z" fill="currentColor"/>
</marker>
</defs>
<rect class="box" x="10" y="100" width="110" height="50"/>
<text class="lbl" x="22" y="130">Client (web/app)</text>
<rect class="box" x="170" y="100" width="110" height="50"/>
<text class="lbl" x="182" y="122">API Gateway</text>
<text class="lbl" x="182" y="138">+ WAF</text>
<rect class="box" x="330" y="100" width="110" height="50"/>
<text class="lbl" x="342" y="122">Lambda</text>
<text class="lbl" x="342" y="138">Order service</text>
<rect class="box" x="490" y="30" width="150" height="50"/>
<text class="lbl" x="500" y="52">Payment processor</text>
<text class="lbl" x="500" y="68">(tokenized, PCI-SAQ-A)</text>
<rect class="box" x="330" y="190" width="110" height="50"/>
<text class="lbl" x="342" y="212">EventBridge</text>
<text class="lbl" x="342" y="228">payment.confirmed</text>
<rect class="box" x="490" y="190" width="150" height="50"/>
<text class="lbl" x="505" y="212">Fulfillment / Notify</text>
<text class="lbl" x="505" y="228">(SQS consumers)</text>
<path class="arrow" d="M120,125 L170,125"/>
<path class="arrow" d="M280,125 L330,125"/>
<path class="arrow" d="M440,120 L490,80"/>
<path class="arrow" d="M385,150 L385,190"/>
<path class="arrow" d="M440,215 L490,215"/>
</svg>
Client submits a tokenized payment method (never a raw card number — that handoff happens client-side via the processor's JS SDK) → API Gateway, protected by AWS WAF, fronts a Lambda-based order service → the order service calls the payment processor's API with the token → on success, it publishes a payment.confirmed event to EventBridge → downstream consumers (fulfillment, email, inventory) react independently.
Idempotency: The Detail That Prevents Double Charges
Network retries are normal — a client times out waiting for a response and retries, or API Gateway retries a Lambda invocation. Without idempotency, that retry can create a second charge for the same order.
import boto3
import hashlib
dynamodb = boto3.resource("dynamodb")
idempotency_table = dynamodb.Table("payment-idempotency-keys")
def process_payment(order_id, amount, idempotency_key):
# Atomically claim this idempotency key before calling the payment processor
try:
idempotency_table.put_item(
Item={"idempotency_key": idempotency_key, "order_id": order_id, "status": "processing"},
ConditionExpression="attribute_not_exists(idempotency_key)"
)
except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
# Already processed or in-flight — return the prior result instead of charging again
existing = idempotency_table.get_item(Key={"idempotency_key": idempotency_key})
return existing["Item"]
# Safe to call the payment processor exactly once for this key
result = call_payment_processor(order_id, amount, idempotency_key)
idempotency_table.update_item(
Key={"idempotency_key": idempotency_key},
UpdateExpression="SET #s = :s, charge_id = :c",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={":s": "completed", ":c": result["charge_id"]}
)
return result
The ConditionExpression="attribute_not_exists(idempotency_key)" is what makes the claim atomic under concurrent retries — only one request wins the write.
Reducing PCI DSS Scope
PCI DSS compliance effort scales directly with how much of your infrastructure can see, store, or transmit raw cardholder data:
- SAQ-A (lightest): you never touch card data at all — it goes straight from the customer's browser to the processor via an iframe or hosted field, and your servers only ever see a token.
- SAQ-A-EP: your page controls the payment form's rendering even though data posts directly to the processor — slightly more scope.
- SAQ-D (heaviest): your systems process, store, or transmit raw card data directly — full audit of nearly every control in PCI DSS.
Architecting for SAQ-A by using the processor's client-side tokenization SDK (Stripe Elements, Braintree Drop-in, Adyen Web Components) is usually the single highest-leverage security decision in the whole system — it removes entire categories of infrastructure from audit scope.
Securing the API Layer
aws wafv2 create-web-acl \
--name payment-api-protection \
--scope REGIONAL \
--default-action Allow={} \
--rules '[
{
"Name": "RateLimitCheckout",
"Priority": 0,
"Statement": {
"RateBasedStatement": { "Limit": 200, "AggregateKeyType": "IP" }
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimitCheckout"
}
}
]' \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=PaymentAPI
Rate-limiting the checkout endpoint specifically defends against card-testing attacks, where attackers script small, repeated authorization attempts against stolen card numbers to find valid ones.
Audit Trail: Two Different Logs for Two Different Needs
- CloudTrail — records that
lambda:InvokeFunctionordynamodb:PutItemhappened, who called it, and when. Essential for infrastructure-level forensics. - Application-level payment ledger — an append-only DynamoDB or Aurora table recording every payment state transition (
created→authorized→captured→refunded) with timestamps and actor IDs. This is what finance and compliance actually query, and CloudTrail alone can't answer "what was this order's payment history."
FAQ
Do I need to store credit card numbers to build a payment system on AWS? No, and you generally shouldn't. Use a PCI-compliant processor's tokenization SDK so raw card data never reaches your servers — this keeps your PCI DSS scope to the lightest tier (SAQ-A).
What AWS service handles the actual payment processing? None does end-to-end — AWS provides the orchestration layer (API Gateway, Lambda, EventBridge, DynamoDB). The actual card authorization and settlement happens through a third-party PCI-certified processor like Stripe or Adyen.
How do I prevent duplicate charges from retried requests? Use idempotency keys — a unique key per payment attempt, checked atomically (e.g., a DynamoDB conditional write) before calling the payment processor, so a retry returns the original result instead of charging again.
Why does PCI DSS scope matter for architecture decisions? Scope determines audit burden. Keeping raw card data entirely out of your infrastructure (SAQ-A) can mean the difference between a short self-assessment and a full annual audit covering most of your stack.
Build Your Payment Architecture Right the First Time
f9infotech designs PCI-scope-reducing payment architectures on AWS — tokenization boundaries, idempotent order processing, and event-driven fulfillment that scales. Talk to our AWS architects before you write the first checkout line of code.