Design Document: Note-Taking Application
Overview
The note-taking application is a React-based single-page application (SPA) that provides authenticated users with the ability to create, view, and edit notes organized in a flat folder structure. The application leverages AWS Amplify for authentication and direct DynamoDB access without a backend server.
The architecture follows a serverless, client-side approach where:
- AWS Cognito handles user authentication and identity management
- DynamoDB stores all application data with user-level isolation
- The React frontend directly accesses DynamoDB using Amplify credentials
- All business logic executes in the browser
This design focuses on Milestone h: establishing a working foundation with basic CRUD operations, authentication, and a simple UI. The implementation prioritizes incremental development with each step producing a runnable system.
Architecture
High-Level Architecture
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ React Application (Vite) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ Authenticator│ │ Forest View │ │ Note Editor│ │ │
│ │ │ Component │ │ Component │ │ Component │ │ │
│ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ AWS Amplify Client Library │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────┐
│ AWS Cloud │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Cognito User │ │ Cognito Identity│ │
│ │ Pool │────────▶│ Pool │ │
│ │ (Auth) │ │ (Credentials) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │
│ │ IAM Role │
│ ▼ │
│ ┌──────────────────┐ │
│ │ DynamoDB │ │
│ │ Table │ │
│ │ (User Data) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Component Responsibilities
React Application Layer:
- Renders UI components for authentication, note listing, and note editing
- Manages application state (current note, forest structure, UI state)
- Handles user interactions and input validation
- Orchestrates data operations through Amplify
AWS Amplify Layer:
- Provides authentication UI (Authenticator component)
- Manages authentication state and session
- Obtains AWS credentials from Cognito Identity Pool
- Provides DynamoDB client with automatic credential management
AWS Cognito:
- User Pool: Authenticates users with username/password
- Identity Pool: Exchanges authenticated user tokens for temporary AWS credentials
- Enforces invitation-only access model
DynamoDB:
- Stores all application data (forest structure, notes, folders)
- Enforces user isolation through IAM policies (PK = Cognito identity)
- Provides transactional writes for atomic updates
Components and Interfaces
Data Access Layer
DynamoDB Client Interface:
interface DynamoDBClient {
// Read operations
getItem(params: GetItemParams): Promise<Item | null>
batchGetItems(keys: ItemKey[]): Promise<Item[]>
// Write operations
putItem(params: PutItemParams): Promise<void>
transactWrite(items: TransactWriteItem[]): Promise<void>
}
interface GetItemParams {
pk: string // Cognito identity
sk: string // Sort key (FOREST, NOTE#id, FOLDER#id)
}
interface PutItemParams {
pk: string
sk: string
attributes: Record<string, any>
}
interface TransactWriteItem {
operation: 'Put' | 'Update' | 'Delete'
pk: string
sk: string
attributes?: Record<string, any>
}
Forest Management
Forest Structure:
interface Forest {
[nodeId: string]: ForestNode
}
interface ForestNode {
type: 'folder' | 'note'
children?: string[] // Only for folders
}
// Example forest:
const exampleForest: Forest = {
"root": { type: "folder", children: ["xyz", "abc"] },
"bin": { type: "folder", children: [] },
"xyz": { type: "folder", children: ["def"] },
"abc": { type: "note" },
"def": { type: "note" }
}
Forest Operations:
interface ForestManager {
// Read forest from DynamoDB
loadForest(): Promise<Forest>
// Initialize empty forest if none exists
initializeForest(): Promise<Forest>
// Add a note to a folder in the forest
addNoteToFolder(forest: Forest, folderId: string, noteId: string): Forest
// Get all note IDs from the forest
getAllNoteIds(forest: Forest): string[]
// Save forest to DynamoDB
saveForest(forest: Forest): Promise<void>
}
Note Management
Note Data Model:
interface Note {
id: string // nanoid
title: string
content: string
createdAt: string // ISO 8601 timestamp
updatedAt: string // ISO 8601 timestamp
}
interface NoteRecord {
PK: string // Cognito identity
SK: string // "NOTE#{id}"
title: string
content: string
createdAt: string
updatedAt: string
}
Note Operations:
interface NoteManager {
// Create a new note and update forest atomically
createNote(folderId: string, title: string, content: string): Promise<Note>
// Read a single note by ID
getNote(noteId: string): Promise<Note | null>
// Batch read note titles for forest display
getNoteMetadata(noteIds: string[]): Promise<Map<string, string>>
// Update an existing note
updateNote(noteId: string, updates: Partial<Note>): Promise<void>
}
Folder Management
Folder Data Model:
interface Folder {
id: string // nanoid
name: string
}
interface FolderRecord {
PK: string // Cognito identity
SK: string // "FOLDER#{id}"
name: string
}
Folder Operations:
interface FolderManager {
// Create a new folder
createFolder(name: string): Promise<Folder>
// Get folder details
getFolder(folderId: string): Promise<Folder | null>
// Batch read folder names for forest display
getFolderMetadata(folderIds: string[]): Promise<Map<string, string>>
}
UI Components
App Component:
function App() {
// Wraps entire app with Amplify Authenticator
// Manages authenticated user state
// Renders ForestView when authenticated
}
ForestView Component:
interface ForestViewProps {
userId: string // Cognito identity
}
function ForestView({ userId }: ForestViewProps) {
// Loads forest on mount
// Displays forest as nested list
// Handles note selection
// Renders NoteEditor when note selected
}
NoteEditor Component:
interface NoteEditorProps {
noteId: string
onClose: () => void
}
function NoteEditor({ noteId, onClose }: NoteEditorProps) {
// Loads note content
// Provides editable title input
// Provides editable content textarea
// Handles save operation
}
Data Models
DynamoDB Table Schema
Table Configuration:
- Table Name:
NotesTable(configured via Amplify) - Partition Key (PK): String - Cognito identity ID
- Sort Key (SK): String - Record type identifier
- Billing Mode: On-demand (for sandbox/development)
Record Types:
1. Forest Record:
PK: "us-east-1:12345678-1234-1234-1234-123456789abc"
SK: "FOREST"
forest: {
"root": { "type": "folder", "children": ["note1", "folder1"] },
"bin": { "type": "folder", "children": [] },
"note1": { "type": "note" },
"folder1": { "type": "folder", "children": [] }
}
2. Note Record:
PK: "us-east-1:12345678-1234-1234-1234-123456789abc"
SK: "NOTE#abc123xyz"
title: "My First Note"
content: "This is the content of my note."
createdAt: "2024-01-15T10:30:00.000Z"
updatedAt: "2024-01-15T14:45:00.000Z"
3. Folder Record:
PK: "us-east-1:12345678-1234-1234-1234-123456789abc"
SK: "FOLDER#def456uvw"
name: "Work Notes"
Access Patterns
1. Load Forest:
- Operation: GetItem
- Key: PK = user identity, SK = “FOREST”
- Frequency: Once per session
- Returns: Complete forest structure
2. Load Note Metadata (Batch):
- Operation: BatchGetItem
- Keys: PK = user identity, SK = “NOTE#{id}” for each note
- Frequency: Once per forest load
- Returns: Titles for all notes in forest
3. Load Single Note:
- Operation: GetItem
- Key: PK = user identity, SK = “NOTE#{id}”
- Frequency: Each time user clicks a note
- Returns: Complete note with content
4. Create Note:
- Operation: TransactWriteItems
- Items:
- Put NOTE#{id} record
- Update FOREST record
- Frequency: Each time user creates a note
- Ensures: Atomic creation of note and forest update
5. Update Note:
- Operation: PutItem
- Key: PK = user identity, SK = “NOTE#{id}”
- Frequency: Each time user saves a note
- Updates: title, content, updatedAt
IAM Policy for User Isolation
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:BatchGetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:*:*:table/NotesTable",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
}
}
}
]
}
This policy ensures users can only access records where PK matches their Cognito identity.
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: All database operations use user’s Cognito identity as PK
For any database operation (read or write), the partition key (PK) must equal the authenticated user’s Cognito identity.
Validates: Requirements 4.3
Property 2: All database operations include sort key
For any database operation, the sort key (SK) must be present and follow the correct format (FOREST, NOTE#{id}, or FOLDER#{id}).
Validates: Requirements 4.2
Property 3: Forest contains unique node IDs
For any forest structure, all node IDs (keys in the forest object) must be unique.
Validates: Requirements 5.2
Property 4: Forest always contains root and bin
For any forest structure, it must contain both “root” and “bin” as top-level keys.
Validates: Requirements 5.3
Property 5: Folder nodes have children array
For any forest node with type=“folder”, the node must have a “children” property that is an array.
Validates: Requirements 5.4
Property 6: Forest nodes have correct type field
For any forest node, if it represents a note, its type must be “note”; if it represents a folder, its type must be “folder”.
Validates: Requirements 5.5, 5.6
Property 7: Node IDs follow nanoid format
For any generated node ID (for notes or folders), the ID must match the nanoid format (URL-safe characters, appropriate length).
Validates: Requirements 5.7, 9.1
Property 8: Note records use correct SK format
For any note record in DynamoDB, the sort key must match the pattern “NOTE#{id}” where {id} is the note’s nanoid.
Validates: Requirements 6.1, 9.2
Property 9: Created notes have all required fields
For any newly created note, the note record must contain all required fields: id, title, content, createdAt, and updatedAt.
Validates: Requirements 6.2, 6.3, 6.4, 6.5
Property 10: Note updates increment timestamp
For any note update operation, the updatedAt timestamp in the saved record must be greater than or equal to the previous updatedAt value.
Validates: Requirements 6.6, 13.2
Property 11: Folder records use correct SK format
For any folder record in DynamoDB, the sort key must match the pattern “FOLDER#{id}” where {id} is the folder’s nanoid.
Validates: Requirements 7.1
Property 12: Created folders have name field
For any newly created folder, the folder record must contain a name field.
Validates: Requirements 7.2
Property 13: Note creation is atomic
For any note creation operation, either both the note record and forest update succeed, or both fail (no partial state).
Validates: Requirements 9.4, 9.5
Property 14: Note creation updates forest
For any successfully created note, the note’s ID must appear in the forest structure.
Validates: Requirements 9.3
Property 15: Empty note titles display placeholder
For any note with an empty or whitespace-only title, the displayed title must be “[no title]”.
Validates: Requirements 10.3
Property 16: Displayed notes are clickable
For any note displayed in the forest view, the note title must have an associated click handler.
Validates: Requirements 10.5
Property 17: Loaded notes display all content
For any loaded note, both the title and content must be visible in the rendered output.
Validates: Requirements 11.2, 11.3
Property 18: Note save round-trip preserves changes
For any note with modified title or content, after saving and reloading the note, the title and content must match the saved values.
Validates: Requirements 13.3, 13.4
Property 19: Database errors display user-friendly messages
For any failed DynamoDB operation, the application must display a user-friendly error message to the user.
Validates: Requirements 14.1
Property 20: Errors are logged to console
For any error that occurs in the application, the error must be logged to the browser console using console.error.
Validates: Requirements 14.4
Error Handling
Authentication Errors
Unauthenticated Access:
- When user is not authenticated, redirect to Amplify Authenticator
- Display login form with username/password fields
- Show clear error messages for invalid credentials
Session Expiration:
- Detect expired credentials when DynamoDB operations fail with auth errors
- Display message to user: “Your session has expired. Please log in again.”
- Redirect to login screen
Network Errors:
- Catch network failures during authentication
- Display message: “Unable to connect. Please check your internet connection.”
- Provide retry option
Database Errors
Read Failures:
- Catch errors from GetItem, BatchGetItem operations
- Log full error to console for debugging
- Display user-friendly message: “Unable to load data. Please try again.”
- Provide retry button
Write Failures:
- Catch errors from PutItem, TransactWriteItems operations
- Log full error to console for debugging
- Display user-friendly message: “Unable to save changes. Please try again.”
- Preserve user’s unsaved changes in UI
- Provide retry button
Transaction Conflicts:
- Handle conditional check failures in transactions
- Retry transaction with exponential backoff (up to 3 attempts)
- If all retries fail, display: “Unable to save due to a conflict. Please refresh and try again.”
Data Validation Errors
Invalid Forest Structure:
- Validate forest structure after loading from DynamoDB
- Check for required “root” and “bin” nodes
- Check that all node types are valid (“note” or “folder”)
- If validation fails, log error and initialize new empty forest
- Display warning: “Your notes structure was corrupted and has been reset.”
Missing Note Data:
- Handle cases where note ID exists in forest but note record doesn’t exist
- Display “[deleted]” or “[missing]” for missing notes
- Log warning to console
- Allow user to continue using app
Malformed Data:
- Validate that loaded notes have required fields
- Provide default values for missing fields (empty string for title/content)
- Log warning about malformed data
- Continue operation with defaults
UI Error States
Loading States:
- Display loading spinner during async operations
- Disable interactive elements during saves
- Show “Loading…” text for clarity
Error Display:
- Use consistent error message component
- Display errors prominently but non-intrusively
- Provide dismiss button for error messages
- Auto-dismiss success messages after 3 seconds
Graceful Degradation:
- If forest fails to load, show empty forest with root and bin
- If note fails to load, show error in note editor area
- Keep app functional even when individual operations fail
Testing Strategy
Overview
The testing strategy employs a dual approach combining unit tests for specific examples and edge cases with property-based tests for universal correctness properties. This ensures both concrete behavior validation and comprehensive input coverage.
Property-Based Testing
Framework: fast-check (JavaScript/TypeScript property-based testing library)
Configuration:
- Minimum 100 iterations per property test
- Each test tagged with feature name and property number
- Tag format:
Feature: note-taking-app, Property {N}: {property description}
Property Test Coverage:
Each correctness property listed in the Correctness Properties section must be implemented as a property-based test. Property tests will:
- Generate random valid inputs (notes, forests, user IDs)
- Execute operations with generated inputs
- Verify that the stated property holds for all generated cases
- Use fast-check’s built-in generators and custom generators for domain objects
Example Property Test Structure:
import fc from 'fast-check';
// Feature: note-taking-app, Property 3: Forest contains unique node IDs
test('forest node IDs are unique', () => {
fc.assert(
fc.property(
forestGenerator(), // Custom generator for valid forests
(forest) => {
const nodeIds = Object.keys(forest);
const uniqueIds = new Set(nodeIds);
expect(uniqueIds.size).toBe(nodeIds.length);
}
),
{ numRuns: 100 }
);
});
Custom Generators:
The test suite will include custom generators for:
- Valid forest structures
- Note objects with random titles and content
- Folder objects with random names
- Cognito identity strings
- Nanoid strings
- Timestamps
Unit Testing
Framework: Vitest (fast unit test framework for Vite projects)
Unit Test Focus:
Unit tests complement property tests by covering:
- Specific examples that demonstrate correct behavior
- Edge cases (empty strings, null values, boundary conditions)
- Integration points between components
- Error conditions and error handling
- UI component rendering and interactions
Test Categories:
1. Component Tests:
- Test that Authenticator component renders when unauthenticated
- Test that ForestView renders forest as nested list
- Test that NoteEditor renders title input and content textarea
- Test that clicking note title triggers note loading
- Test that save button triggers save operation
2. Data Layer Tests:
- Test forest initialization creates root and bin
- Test note creation with specific title and content
- Test note update with specific changes
- Test batch loading of note metadata
- Test transaction rollback on failure
3. Error Handling Tests:
- Test display of authentication errors
- Test display of database errors
- Test display of network errors
- Test error logging to console
- Test retry mechanisms
4. Edge Case Tests:
- Test empty title displays “[no title]”
- Test empty forest initialization
- Test missing note record handling
- Test malformed forest structure handling
- Test session expiration handling
Example Unit Test:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { NoteEditor } from './NoteEditor';
describe('NoteEditor', () => {
it('displays [no title] for empty title', () => {
const note = { id: '123', title: '', content: 'test', createdAt: '...', updatedAt: '...' };
render(<NoteEditor note={note} />);
expect(screen.getByText('[no title]')).toBeInTheDocument();
});
});
Integration Testing
Scope: Integration tests verify end-to-end flows without mocking internal components.
Key Integration Tests:
- Complete note creation flow (create → save → reload → verify)
- Complete note editing flow (load → edit → save → reload → verify)
- Forest loading and display flow
- Authentication flow (login → access data → logout)
Mocking Strategy:
- Mock AWS services (Cognito, DynamoDB) using AWS SDK mocks
- Do not mock internal application components
- Use test fixtures for consistent test data
Test Organization
src/
components/
App.test.tsx
ForestView.test.tsx
NoteEditor.test.tsx
services/
forestManager.test.tsx
noteManager.test.tsx
folderManager.test.tsx
properties/
forest.properties.test.tsx
note.properties.test.tsx
database.properties.test.tsx
integration/
noteCreation.integration.test.tsx
noteEditing.integration.test.tsx
Test Execution
Development:
- Run tests in watch mode during development:
npm run test:watch - Run property tests with verbose output to see generated inputs
- Use Vitest UI for interactive test debugging
CI/CD:
- Run all tests before merge:
npm run test - Require 100% of tests passing
- Generate coverage report (target: >80% coverage)
- Run property tests with increased iterations (500) for thorough validation
Testing Priorities
High Priority (Must Test):
- Data integrity properties (atomicity, user isolation)
- Forest structure correctness
- Note CRUD operations
- Authentication and authorization
- Error handling and recovery
Medium Priority (Should Test):
- UI component rendering
- User interactions (clicks, input changes)
- Loading states
- Edge cases
Low Priority (Nice to Test):
- Performance characteristics
- Accessibility features
- Browser compatibility
Continuous Testing
- Run unit tests on every file save (watch mode)
- Run full test suite before commits (pre-commit hook)
- Run property tests with high iteration count in CI
- Monitor test execution time and optimize slow tests
- Review and update tests when requirements change