Functional Design Docs aren’t corporate overhead. They are a best practice that keeps code lean.
We avoid documentation because "vanilla JS" sounds pure and self-documenting. Then we ship three versions of the same bulk-delete feature and call it "agile."
A Functional Design Document (FDD) explains how a feature gap will be implemented. It converts high-level user needs into a structured functional solution. You need this when your project has more than one developer—or when it takes more than one week of work.
1. Why This Exists
| ✅ With FDD | ❌ Without FDD |
|---|---|
| An FDD explains HOW a feature gap will be implemented. It converts user stories into a structured functional solution everyone agrees on before coding starts. | Lack of structured design leads to confusion, incorrect assumptions, late-stage rework, and "wait, I thought we decided..." debates during code reviews. |
Vanilla JS translation: Before you touch
app.js, write down what the DOM can’t do yet versus what the user actually needs.
2. The Flow That Prevents Rework
Business Need ➔ Fit-Gap Analysis ➔ GAP Identified ➔ FDD Creation
│
Final Solution ◀─── Testing ◀─── Technical Development ◀──┘
Example: Bulk Delete Feature
- Business Need: "Admins need to bulk delete items."
- Fit-Gap: We only have single-item deletion. There are no checkboxes or mass-selection elements in the UI. (GAP Identified)
- FDD Creation: Write the contract first. It takes 20 minutes.
- Technical Development: Write the Vanilla JS (implemented below).
- Testing: Run validation checks against the FDD specs.
- Final Solution: Ships once. No surprise pull request (PR) comments.
3. The 4 Design Docs You Actually Need
| Doc Type | Focus | Example |
|---|---|---|
| FDD (Functional Design) | What it does functionally. | "Clicking 'Delete Selected' removes checked rows and shows a success toast." |
| TDD (Technical Design) | How the code achieves it. | Event delegation, DOM manipulation patterns, memory leak prevention. |
| IDD (Integration Design) | API contracts & payloads. |
DELETE /api/items expects { ids: number[] }
|
| ADD (Analytics Design) | What metrics we track. | Fire bulk_delete_completed event with metadata on success. |
Best Practice: Product managers own the FDD. Developers own the TDD. Both parties sign off on both documents. That is the pattern.
4. FDD Structure + The Actual Vanilla JS
Here is an FDD for a bulk delete feature, paired with the exact Vanilla JS code that satisfies its requirements.
FDD Specification
- GAP: No way to delete multiple table rows simultaneously. The current UI restricts deletion to individual items.
- Solution Overview: Add a checkbox column to the admin table. The header checkbox toggles all selectable rows. A sticky actions bar shows the current selection count and the delete action.
-
Business Logic:
- Skip rows with
data-status="locked". - Display a confirmation prompt before executing deletion.
- Show a success toast notification and fire an analytics event on successful deletion.
- Skip rows with
The Implementation
// admin-table.js - No frameworks, just a clear contract
class BulkDeleteController {
constructor(tableSelector) {
this.table = document.querySelector(tableSelector);
this.selectedIds = new Set();
this.deleteBtn = document.querySelector('#bulk-delete-btn');
this.countEl = document.querySelector('#selected-count');
if (!this.table) return;
this.bindEvents();
this.render();
}
bindEvents() {
// Event delegation: 1 event listener instead of hundreds
this.table.addEventListener('change', (e) => {
if (e.target.matches('.row-checkbox')) {
this.toggleRow(e.target.dataset.id, e.target.checked);
}
if (e.target.matches('#select-all')) {
this.toggleAll(e.target.checked);
}
});
this.deleteBtn?.addEventListener('click', () => this.handleDelete());
}
toggleRow(id, isChecked) {
isChecked ? this.selectedIds.add(id) : this.selectedIds.delete(id);
this.render();
}
toggleAll(isChecked) {
const boxes = this.table.querySelectorAll('.row-checkbox:not([disabled])');
boxes.forEach(box => {
box.checked = isChecked;
this.toggleRow(box.dataset.id, isChecked);
});
}
render() {
const count = this.selectedIds.size;
this.countEl.textContent = count;
this.deleteBtn.disabled = count === 0;
// Update 'select-all' checkbox indeterminate state
const allBoxes = this.table.querySelectorAll('.row-checkbox:not([disabled])');
const selectAll = this.table.querySelector('#select-all');
if (selectAll) {
selectAll.checked = count > 0 && count === allBoxes.length;
selectAll.indeterminate = count > 0 && count < allBoxes.length;
}
}
async handleDelete() {
const ids = [...this.selectedIds].map(Number);
if (!ids.length) return;
const confirmed = confirm(`Delete ${ids.length} items? This action cannot be undone.`);
if (!confirmed) return;
this.deleteBtn.disabled = true;
this.deleteBtn.textContent = 'Deleting...';
try {
const res = await fetch('/api/items', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
});
if (!res.ok) throw new Error('Delete operation failed');
// FDD Requirement: Show toast on success
this.showToast(`Successfully deleted ${ids.length} items`);
// FDD Requirement: Cleanly remove rows from the DOM
ids.forEach(id => {
this.table.querySelector(`tr[data-id="${id}"]`)?.remove();
});
// FDD Requirement: Trigger analytics
window.analytics?.track('bulk_delete_count', { count: ids.length });
this.selectedIds.clear();
} catch (err) {
this.showToast('Failed to delete items. Please try again.', 'error');
} finally {
this.render();
this.deleteBtn.textContent = 'Delete Selected';
}
}
showToast(msg, type = 'success') {
// Standardized toast implementation defined in our system architecture
console.log(`[${type.toUpperCase()}] ${msg}`);
}
}
// Initialize when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', () => {
new BulkDeleteController('#admin-table');
});
The HTML Structure
This markup matches the exact interface promised by our FDD:
<table id="admin-table">
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr data-id="1">
<td><input type="checkbox" class="row-checkbox" data-id="1"></td>
<td>Item 1</td>
<td>active</td>
</tr>
<tr data-id="2" data-status="locked">
<td><input type="checkbox" class="row-checkbox" data-id="2" disabled></td>
<td>Item 2</td>
<td>locked</td>
</tr>
</tbody>
</table>
<div class="bulk-actions">
<span><span id="selected-count">0</span> selected</span>
<button id="bulk-delete-btn" disabled>Delete Selected</button>
</div>
5. Validation Checks as Code
An FDD ensures that code remains testable and predictable. The functional requirements map directly to our code logic:
| Check | User Action | Expected Behavior | Code Proof |
|---|---|---|---|
| Multi-Select Flow | Check 3 boxes, click Delete | Confirm modal shows accurate count. |
confirm(\Delete ${ids.length} items?)
|
| Select-All Logic | Click header checkbox | All un-locked rows get checked. | querySelectorAll('.row-checkbox:not([disabled])') |
| Role Guard | Load page as a 'viewer' role | No select elements render. | if (!canBulkDelete) return; |
| Empty State | Deselect all active items | Delete button returns to disabled state. | this.deleteBtn.disabled = count === 0; |
Rule of Thumb: If your functional design cannot be directly mapped to a validation check in code like the table above, the design is incomplete.
6. Business Benefits = Engineering Benefits
- Accurate Development: The written code does precisely what the FDD laid out. No guesswork required.
-
Faster Delivery Times: You don't have to pause development mid-sprint to debate browser
confirm()dialogue versus building a custom modal component. It was decided and documented beforehand. - Streamlined Onboarding: A new engineer can read the FDD, inspect the Vanilla JS class, and immediately understand the relationship between feature expectations and technical implementation.
- Minimal Scope Creep: Hard edge cases like "what happens to locked items?" are addressed and validated before staging a single commit.
Conclusion
Code is simply our agreed-upon process, automated.
Writing Vanilla JS doesn't mean skipping best practices. It means choosing lightweight processes that keep you moving. 60 lines of vanilla JS backed by a 1-page FDD is faster, more performant, and more stable than 600 lines of framework boilerplate built without a plan.
Quick FDD Template
Copy and drop this outline into /docs/fdd/ on your next project:
# FDD: [Feature Name]
## 1. GAP Analysis
* What is missing or broken in the current UI/UX?
## 2. Solution Overview
* High-level summary of what we are building.
## 3. UI/UX Changes
* DOM modifications, states (empty, loading, disabled), and user pathways.
## 4. Business Logic & Edge Cases
* Roles/permissions, constraints, and conditional behavior.
## 5. Data & API Contract
* Payload requirements and endpoint changes.
## 6. Validation Checks
* How we prove the feature works as intended.
## 7. Impact Analysis
* What existing features could this change touch or break?
Make this simple documentation change, and your next pull request will focus on code quality, not architectural debate.
Top comments (0)