skills/fp-skills/skills/fp-architecture/SKILL.md
Design system architecture using functional programming principles. Pure core with impure shell, state machines as data, memoization of pure functions, type-driven development with branded types, and event sourcing patterns. Activate when: designing system architecture, separating business logic from I/O, modeling state transitions, implementing caching/memoization, creating domain models, preventing invalid states through types, or when the user mentions architecture, pure core, impure shell, state machine, memoization, branded types, event sourcing, or domain modeling. Works in any language (TypeScript, Python, Go, Rust, Java).
npx skillsauth add olion500/skills fp-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Structure systems around FP principles. Business logic is pure. I/O lives at boundaries. State is data.
The most important FP architecture pattern.
┌─────────────────────────────────┐
│ Impure Shell (thin) │ ← HTTP handlers, DB, file I/O, logging
│ ┌───────────────────────────┐ │
│ │ Pure Core (thick) │ │ ← Business logic, validation, transforms
│ │ No I/O, no state, no DB │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
// PURE CORE — testable with plain values, zero mocks
const calculateDiscount = (order: Order): number =>
order.total > 100 ? order.total * 0.1 : 0;
const applyDiscount = (order: Order, discount: number): Order => ({
...order, total: order.total - discount, discount,
});
const validateOrder = (order: Order): Result<Order, string> =>
order.items.length === 0 ? err("Empty order")
: order.total < 0 ? err("Negative total")
: ok(order);
// IMPURE SHELL — thin, just wiring
async function processOrder(orderId: string) {
const order = await db.getOrder(orderId); // I/O
const validated = validateOrder(order); // pure
if (!validated.ok) return log.error(validated.error);
const discount = calculateDiscount(validated.value); // pure
const final = applyDiscount(validated.value, discount); // pure
await db.saveOrder(final); // I/O
}
Testing: Pure core needs zero mocks. expect(calculateDiscount({total: 150})).toBe(15). Done.
Model state transitions as a pure function: (State, Event) → State.
// States — invalid combinations are unrepresentable
type OrderState =
| { status: "draft"; items: Item[] }
| { status: "submitted"; items: Item[]; submittedAt: Date }
| { status: "paid"; items: Item[]; paidAt: Date; txId: string }
| { status: "cancelled"; reason: string };
type OrderEvent =
| { type: "submit" }
| { type: "pay"; txId: string }
| { type: "cancel"; reason: string };
// Pure transition — every valid path is explicit
const transition = (state: OrderState, event: OrderEvent): Result<OrderState, string> => {
switch (event.type) {
case "submit":
return state.status === "draft"
? ok({ status: "submitted", items: state.items, submittedAt: new Date() })
: err(`Cannot submit from ${state.status}`);
case "pay":
return state.status === "submitted"
? ok({ status: "paid", items: state.items, paidAt: new Date(), txId: event.txId })
: err(`Cannot pay from ${state.status}`);
case "cancel":
return state.status === "draft" || state.status === "submitted"
? ok({ status: "cancelled", reason: event.reason })
: err(`Cannot cancel from ${state.status}`);
}
};
Testing: expect(transition(draftOrder, {type:"submit"})).toEqual(ok({status:"submitted",...})). No DB, no mocks.
Cache pure function results. Safe because same input always → same output.
const memoize = <A extends string | number, R>(fn: (arg: A) => R) => {
const cache = new Map<A, R>();
return (arg: A) => cache.get(arg) ?? (cache.set(arg, fn(arg)), cache.get(arg)!);
};
Python: @functools.lru_cache(maxsize=128). Only memoize pure functions. Memoizing impure functions caches stale data.
Use the type system to make invalid states unrepresentable.
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
const getOrder = (id: OrderId): Order => ...;
getOrder(userId("123")); // TYPE ERROR — can't pass UserId as OrderId
type Email = string & { readonly __brand: "Email" };
const parseEmail = (s: string): Result<Email, string> =>
s.includes("@") ? ok(s as Email) : err("Invalid email");
// Downstream functions know email is already validated
const sendTo = (email: Email, body: string): void => ...;
// BAD: allows emailVerified=true with email=undefined
type User = { name: string; email?: string; emailVerified: boolean };
// GOOD: invalid combination is impossible
type User =
| { name: string; email: null }
| { name: string; email: string; emailVerified: false }
| { name: string; email: string; emailVerified: true; verifiedAt: Date };
Store events, not current state. Derive state by replaying. Natural fit for FP: reduce(events) → state.
type CartEvent =
| { type: "item_added"; item: Item; at: Date }
| { type: "item_removed"; itemId: string; at: Date }
| { type: "discount_applied"; percent: number; at: Date };
// Pure: derive state from event log
const buildCart = (events: readonly CartEvent[]): Cart =>
events.reduce((cart, event) => {
switch (event.type) {
case "item_added": return { ...cart, items: [...cart.items, event.item] };
case "item_removed": return { ...cart, items: cart.items.filter(i => i.id !== event.itemId) };
case "discount_applied": return { ...cart, discount: event.percent };
}
}, emptyCart);
// Time-travel debugging
const cartAtStep5 = buildCart(events.slice(0, 5));
Benefits: Full audit trail. Reproduce any bug by replaying events. Undo = remove last event.
parseEmail, not type assertion)memoize(fetchUser) returns stale data silentlyisSubmitted && !isPaid && !isCancelled) — use discriminated unionsdevelopment
Search and query Elasticsearch/Kibana database models using curl API. Use for querying database models, searching Kibana indices, checking Elasticsearch data, investigating data in Kibana, finding records by ID, searching documents. Supports multiple environments (dev, qa, stage, production-us, production-au, production-eu).
development
Search and analyze Datadog logs and metrics using API for cupixworks-api and cupixworks-worker services. Use when debugging errors, investigating issues, searching logs, analyzing worker jobs, checking Sidekiq logs, querying metrics, or finding specific log entries by class/function names. Supports error/warn/info log levels with 14-day retention for logs.
tools
Create, update, search, transition, link, and read comments on Jira issues via CLI. MUST use this skill whenever the user pastes or mentions any atlassian.net URL (Jira issues, Confluence pages, focusedCommentId links, board links — anything from *.atlassian.net). Also use for: TSLA-* ticket references, JQL searches, issue status changes, reading comments, creating bugs, updating descriptions. This is the ONLY way to interact with Jira/Atlassian — there is no MCP Atlassian available.
development
Search Cupix Watch (Kibana/Elasticsearch) application logs at watch.cupix.com. Use when the user asks to search logs, find errors, debug processing issues, or investigate service behavior. Triggers on keywords like "log", "watch", "kibana", "error log", service names (skat, pano, api, worker, vista), or mentions of cupix processing pipelines.