How I Built a Full-Stack Bookstore App in 10 Days (And What I Learned)

Published: (December 10, 2025 at 04:56 AM EST)
4 min read
Source: Dev.to

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);
        }
    }
}
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).

Back to Blog

Related posts

Read more »