Building an e-commerce platform is about much more than creating product pages and a checkout button. Behind the user interface, there are architectural decisions, security concerns, data consistency challenges, and production problems that can make or break the entire system.
My team and I built an e-commerce website for a Paris-based startup. Throughout the project, we faced different engineering challenges, evaluated trade-offs, and learned lessons that went beyond writing code.
In this article, I'll walk through the main architectural and engineering decisions behind the project, from migrating infrastructure to securing authentication, modeling inventory, preventing inconsistent transactions, and diagnosing production outages.
1. The Journey: From AWS to a Single High-Resource Server
Our initial architecture was built on AWS.
The architecture used several managed services to handle different responsibilities.
After a while, we migrated to another Infrastructure-as-a-Service (IaaS) platform and ended up running most of our services on a single high-resource compute machine.
This change was an important architectural trade-off. Instead of distributing most of our services across multiple infrastructure components, we consolidated them onto a single machine.
The experience highlighted an important lesson: infrastructure decisions should be driven by actual requirements, operational constraints, and trade-offs rather than architectural trends alone.
A simpler deployment can reduce operational complexity, but consolidating services also introduces a larger single point of failure.
2. Why We Chose an API-First Architecture
One of our key decisions was to separate the frontend from the backend.
Server-Side Rendering vs. Client-Side Rendering
With Server-Side Rendering (SSR), the server generates HTML containing the data needed by the client. Depending on the technology, this might involve PHP templates, EJS, or another server-side rendering approach.
In a tightly coupled implementation, the frontend's rendering logic and backend application are closely connected. This can make changing the frontend technology or reusing the backend for other clients more difficult.
Client-Side Rendering (CSR), on the other hand, allows the frontend and backend to communicate through a defined API contract.
The frontend can use a framework such as React, while the backend exposes endpoints that communicate through a protocol such as REST.
We chose React for the frontend and an API-first approach because it gave us greater flexibility.
The backend could potentially serve different clients without requiring a complete redesign, including:
- A web application built with another frontend framework
- A mobile application
- A frontend deployed independently on a Content Delivery Network (CDN)
The main benefit was separation of responsibilities. The frontend handled presentation, while the backend exposed the application's business capabilities through APIs.
Of course, CSR is not automatically better than SSR. Rendering strategy should depend on requirements such as SEO, performance, caching, and the user experience. In our case, frontend flexibility was a major consideration.
3. Authentication Architecture: JWTs, Cookies, and Sessions
Authentication was another important part of the project.
One security risk we considered was Cross-Site Scripting (XSS). An XSS vulnerability can allow an attacker to execute malicious JavaScript in a user's browser.
If sensitive authentication tokens are stored in JavaScript-accessible storage, such as localStorage, an attacker who successfully exploits an XSS vulnerability may be able to steal them.
Why HttpOnly Cookies?
We used HttpOnly cookies to prevent client-side JavaScript from directly reading authentication cookies.
The browser automatically includes eligible cookies in requests to the backend, subject to cookie settings and browser rules.
This reduces the risk of token theft through JavaScript, although it does not eliminate XSS or other authentication risks.
Why Use Sessions and JWTs Together?
We also used JWTs as a way to represent information associated with an authenticated user, such as their role.
For example, users might have roles such as:
clienteditorsuperadmin
Sessions and JWTs serve different purposes, and using both requires a clear reason and lifecycle design.
In our implementation, session rotation was our primary strategy for invalidating sessions after a suspected compromise. Rotating session identifiers forces affected clients to authenticate again when the old sessions are invalidated.
JWTs can also be rotated or revoked through appropriate mechanisms, but doing so introduces additional considerations.
The important lesson is that authentication architecture should be designed around the threat model and invalidation requirements, not simply around the popularity of a technology.
Role-Based Access Control (RBAC)
We used Role-Based Access Control (RBAC) to secure API endpoints.
RBAC made sense for our permission model because access could be granted according to a user's assigned role.
For applications requiring more granular authorization based on attributes such as ownership, resource properties, or environmental conditions, Attribute-Based Access Control (ABAC) could be a better fit.
The authorization model should match the complexity of the business requirements.
4. Modeling Inventory Across Multiple Stocks
One unexpected challenge involved inventory management.
Our client stored merchandise across multiple stock locations. Initially, we didn't have that information, so the application had been designed around a single stock location during checkout.
Fortunately, our database model was flexible enough to accommodate the new requirement.
We adapted the application so that a single product in an order could be fulfilled from one or multiple stock locations, using different quantities.
For example, if a customer ordered 10 units of a product, the application could allocate:
- 6 units from Stock A
- 4 units from Stock B
The checkout process had to handle these allocations without introducing discrepancies in inventory records.
This experience reinforced the importance of flexible data modeling.
Business requirements change. A good data model should make reasonable changes possible without requiring a complete redesign.
5. Modeling the Order Lifecycle as a State Machine
An order doesn't simply move from creation to completion in one step.
It passes through different stages, and the application needs to manage those transitions consistently.
Finite State Machines (FSMs) define the states an entity can occupy and the transitions permitted between them.
We used the concept of state machines to reason about the overall checkout process.
Thinking in terms of explicit states helps developers understand which operations are valid at each stage and which transitions should be rejected.
For example, an order should not be treated as successfully paid merely because it has been created.
Separating order states and transitions makes complex business processes easier to reason about, test, and maintain.
6. Securing Database Access Through SSH Tunnels
Many e-commerce deployments built around content management systems use web-based database administration tools such as phpMyAdmin.
These tools can be useful, but exposing a database administration interface publicly introduces another potential attack surface.
Attackers may attempt to guess credentials, exploit vulnerabilities in the administration software, or gain unauthorized access through misconfiguration.
Our Approach: SSH Tunneling
We restricted database network access so that only the authorized Linux server could connect directly to the database.
When we needed to access the database from our local development machines, we connected to the server through SSH and created a tunnel that forwarded traffic to the database.
Conceptually, the SSH server acted as a secure bridge between the development machine and the database.
This approach reduced direct exposure of the database and allowed us to administer it without making the database publicly accessible.
Network restrictions should still be combined with strong authentication, least-privilege database accounts, and appropriate SSH configuration.
The principle is straightforward: if a service doesn't need to be publicly accessible, don't expose it unnecessarily.
7. Hardening a Production Linux Server
Securing the server was another important part of running the application in production.
We applied several measures to reduce unnecessary exposure and improve the server's security posture.
We restricted inbound traffic to the ports required by the application:
- Port 80: HTTP requests, redirected to HTTPS
- Port 443: HTTPS traffic
- Port 22: SSH administration
The SSH port was necessary for remote administration, but it still needed appropriate access restrictions.
Depending on the deployment, additional hardening measures may include SSH key authentication, disabling password-based SSH login, applying security updates, monitoring logs, and limiting administrative privileges.
The goal of server hardening is to minimize the number of unnecessary entry points and reduce the impact of potential attacks.
8. Why We Used an ORM and Database Migrations
An Object-Relational Mapper (ORM) helped us manage the relationship between our JavaScript application and the underlying relational database.
In our case, the ORM provided two particularly useful capabilities:
- A way to map database structures and records to application objects
- A mechanism for managing database schema migrations
Why Migrations Matter
Production databases evolve alongside applications.
We may need to add a column, modify a table, introduce a relationship, or change how information is represented.
Without a structured migration process, schema changes can become difficult to track and reproduce. Poorly planned changes can also introduce data consistency problems or make rollbacks difficult.
Migrations provide a history of schema changes and a repeatable way to apply them across environments.
An ORM also allows developers to perform database queries programmatically rather than manually constructing every SQL statement.
However, ORMs don't eliminate the need to understand SQL, database constraints, indexes, transactions, or query performance.
For a production e-commerce application, these fundamentals remain essential.
9. The Cost of Neglecting Regression Testing
Testing was one of the things we initially neglected.
Eventually, we learned why that was a mistake.
When fixing one bug, we sometimes introduced another problem in a part of the application that had previously worked correctly.
This is exactly the kind of problem regression testing is designed to detect.
Regression testing involves running existing tests after code changes to verify that previously working functionality continues to behave as expected.
For an e-commerce application, regression tests can cover critical workflows such as:
- Adding products to a cart
- Calculating order totals
- Applying inventory changes
- Processing checkout operations
- Handling payment responses
- Sending order-related emails
Not every test needs to be an end-to-end test. Unit tests and integration tests can catch many problems earlier and more efficiently.
The lesson we learned was simple: a feature isn't truly finished if we can't confidently verify that it works after the next change.
10. Designing a Hybrid Image Storage Strategy
Product images can be added, modified, and removed throughout the lifetime of an e-commerce application.
Frontend build assets are different. They are generated during deployment and are often good candidates for CDN delivery.
Product images, however, are dynamic assets that benefit from an explicit storage strategy.
We wanted to avoid relying entirely on an external object storage service while also avoiding a design that depended exclusively on local files.
Content-Addressable Storage
We implemented a hybrid approach in which uploaded file contents were hashed.
Content-addressable storage uses a content-derived identifier to identify a file. This can help determine whether two files contain the same data and can support consistent file management.
We stored copies both locally and remotely.
The local copy was updated when a file with the corresponding identifier was uploaded, while the remote copy was managed through a wrapper abstraction.
This abstraction helped keep the rest of the application independent of the details of the remote storage provider.
The broader lesson is that storage implementations should be replaceable where practical, particularly when external services are involved.
11. Designing a Fallback Strategy for External Storage
Using external storage introduces a dependency on another service.
If that service becomes unavailable, image delivery can be affected.
We therefore implemented a fallback mechanism for product images.
The application attempted to fetch images from the external storage endpoints first.
If an image couldn't be retrieved, it could fall back to the corresponding image in a publicly accessible directory on our own server.
This gave us an alternative delivery path if the external storage service experienced a problem.
Fallback strategies need careful implementation: missing files, permissions, caching, and actual provider outages should be distinguished appropriately.
The important principle is to identify critical external dependencies and decide what the application should do when they fail.
12. Checkout Consistency, Transactions, and Race Conditions
One of the most important challenges in the project was maintaining data consistency during checkout.
A checkout operation can involve several database writes. For example, the application may need to create an order, insert its line items, record payment-related information, and update inventory.
What happens if some operations succeed but another fails?
Without appropriate safeguards, the database could be left in a partially updated state.
Database Transactions
We used database transactions to group related operations into a unit of work.
If all the required operations succeed, the transaction can be committed. If a failure occurs before the transaction commits, it can be rolled back.
Transactions helped us avoid partial writes across the database operations included in the transaction.
However, transactions alone don't automatically solve every concurrency problem or guarantee consistency across external services. Those require additional design decisions.
Inventory Updates and Database Triggers
We also used database triggers to automatically reduce product quantities in the relevant stock locations after payment confirmation.
This helped automate inventory updates, but it also made the correctness of the payment-confirmation flow especially important.
In production systems, payment events should be processed with appropriate idempotency and concurrency safeguards so that duplicate notifications don't cause repeated inventory deductions.
Race Conditions
Race conditions were among the trickiest issues to identify.
We encountered one involving emails.
Asynchronous execution can create situations where operations run in an unexpected order or where multiple operations interfere with one another.
These problems can be difficult to reproduce because they may depend on timing.
The experience taught us to pay close attention to asynchronous workflows, especially when they involve payment processing, inventory updates, notifications, or other side effects.
Integrating Payment Gateways and Delivery Platforms
Another architectural challenge involved integrating external payment gateways and delivery platforms.
Different providers often return different data structures.
Instead of creating entirely separate database tables for every service, we generalized the common fields and stored provider-specific fields in a JSON column.
This approach allowed us to support different providers without making the database schema unnecessarily rigid.
The trade-off is that JSON-based fields require careful validation and can make querying provider-specific information more complex.
The goal was to keep the common data model consistent while preserving the flexibility needed for external integrations.
13. Diagnosing Production Outages Caused by Memory Pressure
One of the more surprising problems we encountered occurred when the Linux server became completely unresponsive, sometimes even preventing SSH access.
After investigating the problem, we discovered that the machine had no swap space configured.
What Is Swap?
Swap is disk space that the operating system can use to hold memory pages when physical RAM is under pressure.
When RAM becomes exhausted, swap can sometimes provide additional breathing room. However, disk storage is much slower than RAM, and heavy swapping can significantly degrade performance.
Without swap, a system under severe memory pressure may become unstable or invoke the operating system's out-of-memory mechanisms.
Adding swap can help in some situations, but it isn't a substitute for sufficient RAM or proper resource management.
The experience reinforced an important operational lesson: understanding the server's memory behavior is just as important as understanding the application's code.
Monitoring RAM usage, investigating memory leaks, and configuring sensible resource limits can help prevent similar incidents.
14. Why We Put Nginx in Front of Express
We needed a reliable entry point for incoming HTTP and HTTPS requests without exposing the Express application directly to the internet.
We therefore placed Nginx in front of Express.
Nginx handled incoming connections, served static assets where appropriate, terminated HTTPS, and forwarded application requests to Express.
This separation provided several benefits:
- A dedicated entry point for incoming traffic
- Centralized TLS configuration
- Reverse proxying to the application server
- An appropriate place to configure request limits and other connection-level policies
The architecture also allowed Express to focus on application logic rather than handling every aspect of public-facing traffic itself.
15. Separating Production and Testing Environments
We maintained separate testing and production environments.
This allowed us to test API endpoints and external integrations using sandbox API keys before deploying changes to the live application.
Keeping environments separate also helps prevent accidental interactions with production data and services during development.
A useful separation strategy includes distinct credentials, environment-specific configuration, and controlled deployment procedures.
The objective is to catch problems before they reach real customers.
Final Thoughts
Building this e-commerce platform taught us that software engineering involves much more than implementing features.
The project required us to think about infrastructure, security, database design, application architecture, asynchronous execution, and production reliability.
Some of our most valuable lessons came from unexpected requirements and problems that appeared only after the application was running.
If I had to summarize the main takeaways, they would be:
- Choose architecture based on requirements. More infrastructure doesn't automatically mean a better solution.
- Keep system boundaries clear. An API-first design can make the frontend and backend easier to evolve independently.
- Treat security as a system-wide concern. Authentication, authorization, network restrictions, and server hardening all matter.
- Design for changing business requirements. Flexible data models can make unexpected changes easier to accommodate.
- Protect data consistency. Transactions, concurrency control, and carefully designed workflows are essential for checkout.
- Invest in regression testing. Fixing one bug shouldn't introduce another.
- Plan for failures. External services, limited resources, and asynchronous operations can all behave in unexpected ways.
- Learn from production. Some of the most important engineering lessons only become visible when a system is operating under real conditions.
The best architecture isn't necessarily the most complex one. It's the one that meets the requirements, makes trade-offs explicit, and remains maintainable as the product evolves.
I'd love to hear from other developers: What's the most important lesson you've learned from building or maintaining a production e-commerce system?





















Top comments (0)