How I Built a Full-Stack Bookstore App in 10 Days (And What I Learned)
The Challenge
Ten days. That's what I had to build a production-ready online bookstore from scratch as part of Amazon's ATLAS training program.
The requirements seemed straightforward at first: users should be able to browse books, add them to a cart, and checkout. But then came the "nice-to-haves" that quickly became must-haves in my mind:
- Personalized recommendations
- Browsing history
- Admin panel with bulk operations
- An inventory system inspired by Amazon's ASIN
Here's what I learned building it—and the bugs that almost broke me.
The Tech Stack
Before diving into code, let me share what I used:
Backend:
- Java 17 + Spring Boot 3
- DynamoDB (with Local for development)
- Maven
Frontend:
- React 18 + Vite
- Material-UI v5
- React Router v6
DevOps:
- GitHub Actions for CI/CD
- Docker for DynamoDB Local
Why this stack? Spring Boot gave me rapid development with built-in dependency injection. DynamoDB forced me to think in NoSQL patterns (no JOINs allowed!). React + MUI meant I could focus on functionality rather than CSS battles.
Challenge #1: The Duplicate Books Nightmare
The Problem
Day 3. I'm feeling good. The admin panel works, bulk upload is functional, and I can import 100 books from a CSV file.
Then I run the import again.
Now I have 200 books. The same 100 books, duplicated.
The Hobbit (ID: a1b2c3d4)
The Hobbit (ID: e5f6g7h8) // Same book, different ID!
The Hobbit (ID: i9j0k1l2) // I ran it three times...
The culprit? Random UUIDs.
// The problematic code
public void saveBook(Book book) {
if (book.getId() == null) {
book.setId(UUID.randomUUID().toString()); // Different every time!
}
bookTable.putItem(book);
}
The Solution: Deterministic IDs
I needed the same book to always get the same ID. The solution? Hash the book's "natural key" (title + author):
public class DeterministicId {
public static String forBook(String title, String author) {
String input = (title + "|" + author).toLowerCase().trim();
byte[] hash = sha1(input);
return "b-" + toHex(hash).substring(0, 12);
}
private static byte[] sha1(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(input.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Now "The Hobbit" by "J.R.R. Tolkien" always generates b-7a3f2e1d9c8b. Import the same CSV 100 times? Still just one copy.
Key Takeaway: When you need idempotent operations, use deterministic identifiers based on business keys, not random UUIDs.
Challenge #2: Building an Amazon-Style ASIN System
Amazon uses ASINs (Amazon Standard Identification Numbers) like B0CHXLZ5P3 for every product. I wanted the same for my bookstore.
The Implementation
public class AsinGenerator {
private static final String CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String generateFromBook(String title, String author) {
String input = (title + "|" + author).toLowerCase().trim();
byte[] hash = sha256(input);
StringBuilder sb = new StringBuilder("B0"); // Amazon format
for (int i = 0; i < 8; i++) {
int index = Math.abs(hash[i] % CHARS.length());
sb.append(CHARS.charAt(index));
}
return sb.toString(); // e.g., "B0XK7M2N9P"
}
public static boolean isValid(String asin) {
return asin != null
&& asin.length() == 10
&& asin.startsWith("B0")
&& asin.matches("[A-Z0-9]{10}");
}
}
Why deterministic? Same reason as IDs—I wanted the same book to always get the same ASIN. This enables:
- Bulk updates by ASIN (not just internal ID)
- Human-readable identifiers for admin operations
- Collision probability of ~1 in 2.8 trillion (36^8)
Challenge #3: Browsing History with LinkedList
The requirement: "Show the user's 10 most recently viewed books."
This screamed LinkedList to me:
- O(1) insertion at front
- O(1) removal from end
- Natural FIFO ordering
public class BrowsingHistoryService {
private static final int MAX_CAPACITY = 10;
public void addBookToHistory(String username, String bookId) {
LinkedList<String> history = getHistory(username);
// Remove if already exists (user viewed it before)
history.remove(bookId);
// Add to front (most recent)
history.addFirst(bookId);
// Maintain capacity
while (history.size() > MAX_CAPACITY) {
history.removeLast();
}
saveHistory(username, history);
}
}
The remove(bookId) before addFirst(bookId) handles a subtle edge case: if a user views the same book twice, it should move to the front, not appear twice.
Challenge #4: DynamoDB Said "No" to Empty Strings
Day 5. Everything works locally. I deploy, test the admin panel, and...
DynamoDbException: The AttributeValue for a key attribute
cannot contain an empty string value.
The Problem
When creating a book without providing an ID, my React frontend was sending:
const book = {
id: '', // Empty string, NOT null
title: 'New Book',
author: 'Some Author'
};
DynamoDB's partition keys cannot be empty strings. Null? Fine. Undefined? Fine. Empty string? Absolutely not.
The Solution: Defense in Depth
I fixed it at both layers:
Frontend:
const bookToUpload = {
id: book.id?.trim() || undefined, // Convert empty to undefined
title: book.title,
author: book.author,
};
Backend:
@PostMapping
public ResponseEntity<?> createBook(@RequestBody Book book) {
if (book.getId() == null || book.getId().isBlank()) {
book.setId(DeterministicId.forBook(
book.getTitle(),
book.getAuthor()
));
}
adapter.save(book);
return ResponseEntity.created(uri).build();
}
Key Takeaway: Always validate at API boundaries. What seems like "null" in one language might be an empty string in another.
Challenge #5: Book Covers Were Getting Cropped
I integrated OpenLibrary's free API for book covers:
const coverUrl = `https://covers.openlibrary.org/b/isbn/${isbn}-L.jpg`;
But my covers looked terrible. Half the title was cut off on every book.
The Problem
/* This was cropping images */
img {
width: 100%;
height: 300px;
object-fit: cover; /* Fills space, crops excess */
}
The Solution
Amazon doesn't crop their product images—they show the full cover with padding:
<Box sx={{
height: 380,
background: '#ffffff',
padding: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<img
src={coverUrl}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain', // Shows FULL image
}}
onError={() => setShowFallback(true)}
/>
</Box>
For books without ISBNs, I created gradient fallbacks based on genre:
const genreGradients = {
Fantasy: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
Programming: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
Mystery: 'linear-gradient(135deg, #2c3e50 0%, #4a00e0 100%)',
};
The CI/CD Pipeline That Saved Me
On Day 9, I set up GitHub Actions.
jobs:
backend-build:
runs-on: ubuntu-latest
services:
dynamodb:
image: amazon/dynamodb-local:latest
ports:
- 8000:8000
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Run tests
run: mvn test
- name: Build
run: mvn package -DskipTests
Why this mattered: I pushed a "quick fix" that broke 12 tests. Without CI, I would've deployed broken code. Instead, GitHub showed a red and I fixed it before it became a problem.
Final Stats
After 10 days:
| Metric | Value |
|---|---|
| Backend Classes | 42+ |
| Frontend Components | 25+ |
| Lines of Code | ~6,300 |
| Tests | 134 (passing) |
| Test Coverage | 58% |
| API Endpoints | 20+ |
| Git Commits | 100+ |
What I'd Do Differently
- Start with CI/CD on Day 1, not Day 9
- Write tests as I code, not at the end
- Use TypeScript for the frontend (caught several bugs that types would've prevented)
- Design the DynamoDB schema upfront (migrations are painful)
Key Takeaways
- Deterministic IDs > Random UUIDs when you need idempotent operations
- Validate at API boundaries—empty strings and null are not the same
-
object-fit: containfor product images,coverfor backgrounds - LinkedList is perfect for fixed-capacity, ordered collections
- CI/CD catches bugs before users do
Try It Yourself
The full source code is on GitHub: Online Bookstore Application
Feel free to clone it, break it, and make it better. That's how we learn.
What's the most challenging bug you've faced in a project? I'd love to hear about it in the comments!
Top comments (0)