How I Built a Full-Stack Bookstore App in 10 Days (And What I Learned)
Source: Dev.to
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
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 let me focus on functionality rather than CSS battles.
Challenge #1: The Duplicate Books Nightmare
The Problem
Day 3. The admin panel works, bulk upload is functional, and I can import 100 books from a CSV file.
Running the import again created duplicate entries:
The Hobbit (ID: a1b2c3d4)
The Hobbit (ID: e5f6g7h8) // Same book, different ID!
The Hobbit (ID: i9j0k1l2) // 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 was to 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
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 < 10; i++) {
int idx = (hash[i] & 0xFF) % CHARS.length();
sb.append(CHARS.charAt(idx));
}
return sb.toString();
}
private static byte[] sha256(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(input.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Browsing History (Related Feature)
public class HistoryService {
private static final int MAX_CAPACITY = 20;
public void addToHistory(String username, String bookId) {
Deque<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 the edge case where a user views the same book twice—it moves to the front instead of appearing twice.
Challenge #4: DynamoDB Said “No” to Empty Strings
The Problem
Deploying revealed an exception:
DynamoDbException: The AttributeValue for a key attribute
cannot contain an empty string value.
My React frontend was sending an empty string for id:
const book = {
id: '', // Empty string, NOT null
title: 'New Book',
author: 'Some Author'
};
DynamoDB’s partition keys cannot be empty strings. null or undefined are acceptable.
The Solution: Defense in Depth
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
The Problem
Using OpenLibrary’s cover API:
const coverUrl = `https://covers.openlibrary.org/b/isbn/${isbn}-L.jpg`;
My CSS forced cropping:
img {
width: 100%;
height: 300px;
object-fit: cover; /* Fills space, crops excess */
}
The Solution
Amazon shows full product images with padding. I switched to object-fit: contain inside a styled container:
<img
src={coverUrl}
alt={title}
style={{ width: '100%', height: '300px', objectFit: 'contain' }}
onError={() => setShowFallback(true)}
/>
For books without ISBNs, I created gradient fallbacks (implementation omitted for brevity).