
Data Design
- 62 installs
- 19 repo stars
- Updated January 20, 2026
- miles990/claude-software-skills
Helps with design & ui/ux tasks.
About
data-design is a Claude Code skill for design & ui/ux. It helps solo builders move faster with AI-assisted development.
- data-design
- Design & UI/UX
- AI-coding skill
Data Design by the numbers
- 62 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,201 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/miles990/claude-software-skills --skill data-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 62 |
|---|---|
| repo stars | ★ 19 |
| Last updated | January 20, 2026 |
| Repository | miles990/claude-software-skills ↗ |
What it does
Helps with design & ui/ux tasks.
Files
Data Design
Overview
Principles for designing data structures, schemas, and data flows that are efficient, maintainable, and scalable.
---
Data Modeling
Entity-Relationship Diagrams
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ Order │ │ Product │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ id (PK) │──┐ │ id (PK) │ ┌──│ id (PK) │
│ email │ │ │ user_id(FK) │←───┘ │ name │
│ name │ └───→│ status │ │ price │
│ created_at │ │ total │ │ stock │
└─────────────┘ │ created_at │ └─────────────┘
└─────────────┘ │
│ │
┌──────┴──────┐ │
↓ ↓ │
┌─────────────┐ │
│ OrderItem │ │
├─────────────┤ │
│ id (PK) │ │
│ order_id(FK)│ │
│ product_id │─────────────────────┘
│ quantity │
│ price │
└─────────────┘Relationship Types
| Type | Description | Example |
|---|---|---|
| 1:1 | One to one | User ↔ Profile |
| 1:N | One to many | User → Orders |
| M:N | Many to many | Students ↔ Courses |
-- 1:1 (profile extends user)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE
);
CREATE TABLE profiles (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
bio TEXT,
avatar_url VARCHAR(255)
);
-- 1:N (user has many orders)
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10,2)
);
-- M:N (students ↔ courses via junction table)
CREATE TABLE enrollments (
student_id INTEGER REFERENCES students(id),
course_id INTEGER REFERENCES courses(id),
enrolled_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (student_id, course_id)
);---
Normalization
Normal Forms
| Form | Rule | Example Violation |
|---|---|---|
| 1NF | Atomic values, no repeating groups | tags: "a,b,c" |
| 2NF | 1NF + no partial dependencies | Non-key depends on part of composite key |
| 3NF | 2NF + no transitive dependencies | zip → city in orders table |
| BCNF | Every determinant is a candidate key | Rare edge cases |
-- ❌ Violates 1NF (non-atomic)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
tags VARCHAR(255) -- "electronics,sale,featured"
);
-- ✅ 1NF compliant
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE product_tags (
product_id INTEGER REFERENCES products(id),
tag VARCHAR(50),
PRIMARY KEY (product_id, tag)
);
-- ❌ Violates 3NF (transitive dependency)
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_zip VARCHAR(10),
customer_city VARCHAR(100) -- Depends on zip, not order
);
-- ✅ 3NF compliant
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
zip VARCHAR(10),
city VARCHAR(100)
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id)
);---
Denormalization
When to Denormalize
Normalize for:
✅ Write-heavy workloads
✅ Data integrity requirements
✅ Storage efficiency
✅ Flexibility in queries
Denormalize for:
✅ Read-heavy workloads
✅ Complex joins hurting performance
✅ Reporting/analytics
✅ Known access patternsDenormalization Patterns
-- Computed columns
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
items JSONB,
item_count INTEGER GENERATED ALWAYS AS (jsonb_array_length(items)) STORED,
total DECIMAL(10,2)
);
-- Duplicated data for read performance
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
author_id INTEGER REFERENCES users(id),
author_name VARCHAR(100), -- Duplicated from users
author_avatar VARCHAR(255), -- Duplicated from users
content TEXT
);
-- Materialized view for complex queries
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT
DATE_TRUNC('month', created_at) as month,
product_id,
SUM(quantity) as units_sold,
SUM(total) as revenue
FROM order_items
GROUP BY 1, 2;
-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_sales;---
Schema Design Patterns
Soft Deletes
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255),
deleted_at TIMESTAMP NULL,
-- Partial unique index
CONSTRAINT unique_active_email UNIQUE (email) WHERE deleted_at IS NULL
);
-- Query active users only
SELECT * FROM users WHERE deleted_at IS NULL;Audit Trail
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
table_name VARCHAR(100),
record_id INTEGER,
action VARCHAR(10), -- INSERT, UPDATE, DELETE
old_data JSONB,
new_data JSONB,
changed_by INTEGER REFERENCES users(id),
changed_at TIMESTAMP DEFAULT NOW()
);
-- Trigger for automatic auditing
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_by)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP != 'INSERT' THEN to_jsonb(OLD) END,
CASE WHEN TG_OP != 'DELETE' THEN to_jsonb(NEW) END,
current_setting('app.user_id', true)::INTEGER
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;Multi-Tenancy
-- Row-level security
CREATE TABLE organizations (
id SERIAL PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
org_id INTEGER REFERENCES organizations(id),
name VARCHAR(255)
);
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY org_isolation ON projects
USING (org_id = current_setting('app.org_id')::INTEGER);
-- Set org context per request
SET app.org_id = 123;
SELECT * FROM projects; -- Only sees org 123's projectsVersioning / History
-- Version table pattern
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
current_version_id INTEGER
);
CREATE TABLE document_versions (
id SERIAL PRIMARY KEY,
document_id INTEGER REFERENCES documents(id),
version INTEGER,
content TEXT,
created_at TIMESTAMP DEFAULT NOW(),
created_by INTEGER REFERENCES users(id),
UNIQUE (document_id, version)
);
-- Temporal tables (PostgreSQL)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
price DECIMAL(10,2),
valid_from TIMESTAMP DEFAULT NOW(),
valid_to TIMESTAMP DEFAULT 'infinity'
);
-- Query historical state
SELECT * FROM products
WHERE valid_from <= '2024-01-01' AND valid_to > '2024-01-01';---
NoSQL Schema Design
Document Store (MongoDB)
// Embedded vs Referenced
// ✅ Embed when: data is accessed together, 1:few relationship
{
_id: ObjectId("..."),
title: "Blog Post",
author: {
name: "John",
email: "john@example.com"
},
comments: [
{ user: "Jane", text: "Great post!", date: ISODate("...") }
]
}
// ✅ Reference when: data is accessed independently, 1:many or M:N
{
_id: ObjectId("..."),
title: "Blog Post",
authorId: ObjectId("..."), // Reference to users collection
commentIds: [ObjectId("..."), ObjectId("...")]
}
// ❌ Anti-pattern: Unbounded arrays
{
_id: ObjectId("..."),
logs: [...] // Can grow to millions, hits 16MB limit
}
// ✅ Better: Bucket pattern
{
_id: ObjectId("..."),
sensorId: "sensor-123",
date: ISODate("2024-01-15"),
readings: [...] // Max ~1000 per document
}Key-Value Store (Redis)
# Naming conventions
user:123 # User object
user:123:sessions # User's sessions (set)
user:123:orders # User's orders (list)
order:456 # Order object
orders:pending # Queue of pending orders (list)
products:category:electronics # Products in category (set)
# Expiration patterns
session:{token} # Expires after 30 min
rate_limit:ip:1.2.3.4 # Expires after 1 min
cache:api:/users/123 # Expires after 5 min---
Data Pipeline Design
ETL vs ELT
ETL (Extract, Transform, Load):
Source → Transform (external) → Data Warehouse
Use: Traditional, when transformation is complex
ELT (Extract, Load, Transform):
Source → Data Lake/Warehouse → Transform (in-place)
Use: Modern, leverages warehouse compute powerEvent Sourcing
// Events are the source of truth
interface Event {
id: string;
aggregateId: string;
type: string;
payload: unknown;
timestamp: Date;
version: number;
}
// Event store
class EventStore {
async append(aggregateId: string, events: Event[]) {
await db.events.insertMany(events);
}
async getEvents(aggregateId: string): Promise<Event[]> {
return db.events
.find({ aggregateId })
.sort({ version: 1 })
.toArray();
}
}
// Rebuild state from events
function rebuildAccount(events: Event[]): Account {
return events.reduce((account, event) => {
switch (event.type) {
case 'AccountOpened':
return { balance: 0, ...event.payload };
case 'MoneyDeposited':
return { ...account, balance: account.balance + event.payload.amount };
case 'MoneyWithdrawn':
return { ...account, balance: account.balance - event.payload.amount };
default:
return account;
}
}, {} as Account);
}---
Data Governance
Data Quality Dimensions
| Dimension | Description | Example Check |
|---|---|---|
| Accuracy | Correct values | Email format validation |
| Completeness | No missing data | Required fields present |
| Consistency | Same across systems | User ID matches in all tables |
| Timeliness | Up to date | Last updated within 24h |
| Uniqueness | No duplicates | Unique email per user |
Schema Evolution
-- Safe migrations
-- ✅ Adding nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL;
-- ✅ Adding column with default
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
-- ⚠️ Making column non-null (multi-step)
-- Step 1: Add with default
ALTER TABLE users ADD COLUMN verified BOOLEAN DEFAULT false;
-- Step 2: Backfill data
UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL;
-- Step 3: Add constraint
ALTER TABLE users ALTER COLUMN verified SET NOT NULL;
-- ❌ Dangerous: Renaming column
-- Instead: Add new, migrate data, remove old (over multiple deploys)---
Related Skills
- [[database]] - Database implementation
- [[architecture-patterns]] - Data architecture patterns
- [[api-design]] - Data in APIs
Data Design Templates
Schema patterns and design templates for database modeling.
Files
| Template | Purpose |
|---|---|
schema-patterns.sql | Common database schema patterns |
Schema Patterns
1. Hierarchical Data
For tree structures like categories, org charts, file systems.
-- Self-referencing with materialized path
CREATE TABLE categories (
id UUID PRIMARY KEY,
name VARCHAR(255),
parent_id UUID REFERENCES categories(id),
path TEXT -- '/root/parent/child'
);Use when: Categories, comments (threaded), org structure
2. Many-to-Many with Metadata
Junction tables that carry additional data.
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id),
role_id UUID REFERENCES roles(id),
granted_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
PRIMARY KEY (user_id, role_id)
);Use when: Role assignments, team memberships, enrollments
3. Polymorphic Associations
One table relating to multiple entity types.
-- Type + ID approach
commentable_type VARCHAR(50), -- 'post', 'article'
commentable_id UUID
-- Or separate FKs with constraint
post_id UUID REFERENCES posts(id),
article_id UUID REFERENCES articles(id),
CONSTRAINT one_parent CHECK (...)Use when: Comments, attachments, activity logs
4. Event Sourcing
Append-only event log for audit/replay.
CREATE TABLE events (
stream_id UUID,
event_type VARCHAR(100),
version INTEGER,
payload JSONB,
created_at TIMESTAMPTZ
);Use when: Audit requirements, undo/redo, analytics
5. Temporal Data
Time-based validity periods.
CREATE TABLE prices (
product_id UUID,
price DECIMAL,
valid_from TIMESTAMPTZ,
valid_to TIMESTAMPTZ
);Use when: Price history, versioned data, SCD Type 2
6. EAV (Entity-Attribute-Value)
Dynamic attributes (use JSONB instead when possible).
-- Better: JSONB column
attributes JSONB DEFAULT '{}'
CREATE INDEX ON table USING GIN (attributes);Use when: Highly dynamic attributes, user-defined fields
7. Tagging System
Flexible categorization.
CREATE TABLE tags (id, name, slug);
CREATE TABLE taggings (tag_id, taggable_type, taggable_id);Use when: Content tagging, filtering, faceted search
8. Soft Delete
Archive instead of hard delete.
-- Option A: Flag
deleted_at TIMESTAMPTZ
-- Option B: Archive table
INSERT INTO posts_archive SELECT * FROM posts WHERE id = ?;
DELETE FROM posts WHERE id = ?;Use when: Data recovery needs, audit requirements
Design Guidelines
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Tables | plural, snake_case | user_profiles |
| Columns | snake_case | created_at |
| PKs | id | id UUID |
| FKs | {table}_id | user_id |
| Indexes | idx_{table}_{columns} | idx_users_email |
| Constraints | {table}_{type}_{desc} | users_email_unique |
Column Types
-- IDs
id UUID -- Distributed-friendly
id SERIAL -- Simple auto-increment
-- Strings
VARCHAR(n) -- Known max length
TEXT -- Unknown/large text
-- Numbers
INTEGER -- Whole numbers
DECIMAL(10, 2) -- Money (exact)
REAL / DOUBLE PRECISION -- Scientific (approximate)
-- Dates
TIMESTAMPTZ -- Always use with timezone
DATE -- Date only
INTERVAL -- Duration
-- JSON
JSONB -- Prefer over JSON (indexed)
-- Boolean
BOOLEAN -- true/falseIndex Guidelines
-- Index columns used in:
-- - WHERE clauses
-- - JOIN conditions
-- - ORDER BY
-- - Foreign keys
-- Don't over-index:
-- - Small tables (<1000 rows)
-- - Frequently updated columns
-- - Low-cardinality columns (use partial index)ERD Notation (Mermaid)
erDiagram
USER ||--o{ POST : writes
USER ||--o{ COMMENT : writes
POST ||--o{ COMMENT : has
POST }o--o{ TAG : has
USER {
uuid id PK
string email UK
string name
timestamp created_at
}
POST {
uuid id PK
uuid user_id FK
string title
text content
timestamp published_at
}Generate with: https://mermaid.live
-- ===========================================
-- Data Design Schema Patterns
-- Usage: Reference patterns for common scenarios
-- ===========================================
-- ===========================================
-- Pattern 1: Hierarchical Data (Self-referencing)
-- ===========================================
-- Tree structure (e.g., categories, org chart)
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
parent_id UUID REFERENCES categories(id) ON DELETE CASCADE,
depth INTEGER NOT NULL DEFAULT 0,
path TEXT, -- Materialized path: '/root/parent/child'
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for tree queries
CREATE INDEX idx_categories_parent ON categories (parent_id);
CREATE INDEX idx_categories_path ON categories (path);
-- Recursive query example
-- WITH RECURSIVE category_tree AS (
-- SELECT id, name, parent_id, 0 as level
-- FROM categories WHERE parent_id IS NULL
-- UNION ALL
-- SELECT c.id, c.name, c.parent_id, ct.level + 1
-- FROM categories c
-- JOIN category_tree ct ON c.parent_id = ct.id
-- )
-- SELECT * FROM category_tree;
-- ===========================================
-- Pattern 2: Many-to-Many with Metadata
-- ===========================================
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL
);
-- Roles
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) UNIQUE NOT NULL
);
-- User-Role junction with metadata
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
granted_by UUID REFERENCES users(id),
granted_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
PRIMARY KEY (user_id, role_id)
);
CREATE INDEX idx_user_roles_role ON user_roles (role_id);
-- ===========================================
-- Pattern 3: Polymorphic Associations
-- ===========================================
-- Comments that can belong to multiple entity types
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
body TEXT NOT NULL,
author_id UUID NOT NULL REFERENCES users(id),
-- Polymorphic reference
commentable_type VARCHAR(50) NOT NULL, -- 'post', 'article', 'product'
commentable_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_comments_target ON comments (commentable_type, commentable_id);
-- Alternative: Separate FKs (more type-safe)
-- CREATE TABLE comments (
-- id UUID PRIMARY KEY,
-- body TEXT NOT NULL,
-- post_id UUID REFERENCES posts(id),
-- article_id UUID REFERENCES articles(id),
-- CONSTRAINT one_parent CHECK (
-- (post_id IS NOT NULL)::int + (article_id IS NOT NULL)::int = 1
-- )
-- );
-- ===========================================
-- Pattern 4: Event Sourcing / Audit Trail
-- ===========================================
-- Events table (append-only)
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
stream_id UUID NOT NULL, -- Aggregate ID
stream_type VARCHAR(100) NOT NULL, -- 'Order', 'User'
event_type VARCHAR(100) NOT NULL, -- 'OrderCreated', 'ItemAdded'
version INTEGER NOT NULL,
payload JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (stream_id, version)
);
CREATE INDEX idx_events_stream ON events (stream_id, version);
CREATE INDEX idx_events_type ON events (event_type);
CREATE INDEX idx_events_created ON events (created_at);
-- ===========================================
-- Pattern 5: Temporal Data (Time-based)
-- ===========================================
-- Price history with validity periods
CREATE TABLE product_prices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL,
price DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ, -- NULL = current
-- Prevent overlapping periods
EXCLUDE USING gist (
product_id WITH =,
tstzrange(valid_from, valid_to) WITH &&
)
);
-- Get current price
-- SELECT * FROM product_prices
-- WHERE product_id = 'xxx'
-- AND valid_from <= NOW()
-- AND (valid_to IS NULL OR valid_to > NOW());
-- ===========================================
-- Pattern 6: EAV (Entity-Attribute-Value)
-- ===========================================
-- Dynamic attributes (use sparingly)
CREATE TABLE entity_attributes (
entity_type VARCHAR(50) NOT NULL,
entity_id UUID NOT NULL,
attribute_name VARCHAR(100) NOT NULL,
attribute_value TEXT,
value_type VARCHAR(20) DEFAULT 'string', -- string, number, boolean, json
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (entity_type, entity_id, attribute_name)
);
CREATE INDEX idx_attrs_entity ON entity_attributes (entity_type, entity_id);
-- Better alternative: JSONB column
-- CREATE TABLE products (
-- id UUID PRIMARY KEY,
-- name VARCHAR(255) NOT NULL,
-- attributes JSONB DEFAULT '{}' -- Flexible attributes
-- );
-- CREATE INDEX idx_products_attrs ON products USING GIN (attributes);
-- ===========================================
-- Pattern 7: Tagging System
-- ===========================================
-- Tags
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
category VARCHAR(50),
usage_count INTEGER DEFAULT 0
);
-- Taggings (junction)
CREATE TABLE taggings (
tag_id UUID REFERENCES tags(id) ON DELETE CASCADE,
taggable_type VARCHAR(50) NOT NULL,
taggable_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (tag_id, taggable_type, taggable_id)
);
CREATE INDEX idx_taggings_target ON taggings (taggable_type, taggable_id);
-- Find items by tag
-- SELECT DISTINCT taggable_id FROM taggings
-- WHERE taggable_type = 'post' AND tag_id IN (
-- SELECT id FROM tags WHERE slug IN ('javascript', 'react')
-- );
-- ===========================================
-- Pattern 8: Soft Delete with Archive
-- ===========================================
-- Main table
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title VARCHAR(255) NOT NULL,
content TEXT,
status VARCHAR(20) DEFAULT 'draft',
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Archive table (same structure + archive metadata)
CREATE TABLE posts_archive (
id UUID PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
status VARCHAR(20),
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
-- Archive metadata
archived_at TIMESTAMPTZ DEFAULT NOW(),
archived_by UUID
);
-- Archive function
-- CREATE OR REPLACE FUNCTION archive_post(post_id UUID, user_id UUID)
-- RETURNS VOID AS $$
-- BEGIN
-- INSERT INTO posts_archive
-- SELECT *, NOW(), user_id FROM posts WHERE id = post_id;
-- DELETE FROM posts WHERE id = post_id;
-- END;
-- $$ LANGUAGE plpgsql;