[{"data":1,"prerenderedAt":11590},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-5":4,"blog-paginated-cats":10944},640,[5,826,1655,2474,3736,4539,5625,5951,6197,6462,6798,7586,7951,9108,10647],{"id":6,"title":7,"author":8,"body":11,"category":808,"date":809,"description":810,"extension":811,"featured":812,"image":813,"keywords":814,"meta":817,"navigation":278,"path":818,"readTime":104,"seo":819,"stem":820,"tags":821,"__hash__":825},"blog/blog/database-indexing-strategies.md","Database Indexing Strategies That Actually Make Queries Fast",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":795},"minimark",[14,18,21,26,29,44,48,55,114,117,156,160,163,183,194,205,209,216,233,236,244,285,288,291,314,318,321,371,374,378,381,435,441,445,452,460,494,499,533,543,558,562,565,613,617,620,631,634,645,649,656,717,724,727,742,745,748,759,761,765,791],[15,16,17],"p",{},"Database indexing is the highest-leverage performance optimization available to most application developers. A missing index on a frequently-queried column can be the difference between a 50ms query and a 5000ms query on a table with 1 million rows. Adding the right index takes minutes. Finding the missing index takes knowing where to look.",[15,19,20],{},"This guide is about knowing where to look.",[22,23,25],"h2",{"id":24},"what-an-index-actually-is","What an Index Actually Is",[15,27,28],{},"An index is a separate data structure maintained by the database that allows finding rows matching specific conditions without scanning every row in the table. PostgreSQL's default index type is a B-tree (balanced tree), which keeps keys sorted and supports equality and range lookups efficiently.",[15,30,31,32,36,37,40,41,43],{},"When you run ",[33,34,35],"code",{},"SELECT * FROM users WHERE email = 'james@example.com'"," without an index on ",[33,38,39],{},"email",", PostgreSQL scans every row in the users table to find matches. This is called a sequential scan. With an index on ",[33,42,39],{},", PostgreSQL traverses the B-tree in O(log n) operations and retrieves matching rows directly. On a million-row table, that is the difference between reading 1,000,000 rows and reading about 20.",[22,45,47],{"id":46},"reading-query-plans","Reading Query Plans",[15,49,50,51,54],{},"Before adding indexes, understand what your queries are actually doing. ",[33,52,53],{},"EXPLAIN ANALYZE"," shows the query plan with actual execution costs:",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-sql shiki shiki-themes github-dark","EXPLAIN ANALYZE\nSELECT u.name, COUNT(p.id) as post_count\nFROM users u\nLEFT JOIN posts p ON p.author_id = u.id\nWHERE u.created_at > '2025-01-01'\nGROUP BY u.id, u.name\nORDER BY post_count DESC\nLIMIT 20;\n","sql","",[33,63,64,72,78,84,90,96,102,108],{"__ignoreMap":61},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,71],{},"EXPLAIN ANALYZE\n",[65,73,75],{"class":67,"line":74},2,[65,76,77],{},"SELECT u.name, COUNT(p.id) as post_count\n",[65,79,81],{"class":67,"line":80},3,[65,82,83],{},"FROM users u\n",[65,85,87],{"class":67,"line":86},4,[65,88,89],{},"LEFT JOIN posts p ON p.author_id = u.id\n",[65,91,93],{"class":67,"line":92},5,[65,94,95],{},"WHERE u.created_at > '2025-01-01'\n",[65,97,99],{"class":67,"line":98},6,[65,100,101],{},"GROUP BY u.id, u.name\n",[65,103,105],{"class":67,"line":104},7,[65,106,107],{},"ORDER BY post_count DESC\n",[65,109,111],{"class":67,"line":110},8,[65,112,113],{},"LIMIT 20;\n",[15,115,116],{},"Look for:",[118,119,120,128,134,140,150],"ul",{},[121,122,123,127],"li",{},[124,125,126],"strong",{},"Seq Scan:"," The database is reading every row in the table. A red flag on large tables.",[121,129,130,133],{},[124,131,132],{},"Index Scan:"," Using an index. Good.",[121,135,136,139],{},[124,137,138],{},"Index Only Scan:"," Reading only the index, not the table. Best — means the index covers all needed columns.",[121,141,142,145,146,149],{},[124,143,144],{},"Rows (estimated vs actual):"," Large discrepancies indicate stale statistics. Run ",[33,147,148],{},"ANALYZE table_name"," to refresh them.",[121,151,152,155],{},[124,153,154],{},"Actual Time:"," The actual milliseconds spent on each step.",[22,157,159],{"id":158},"single-column-indexes","Single-Column Indexes",[15,161,162],{},"Start here. Create an index on any column you filter by frequently:",[56,164,166],{"className":58,"code":165,"language":60,"meta":61,"style":61},"CREATE INDEX idx_users_email ON users(email);\nCREATE INDEX idx_posts_author_id ON posts(author_id);\nCREATE INDEX idx_posts_published_at ON posts(published_at DESC);\n",[33,167,168,173,178],{"__ignoreMap":61},[65,169,170],{"class":67,"line":68},[65,171,172],{},"CREATE INDEX idx_users_email ON users(email);\n",[65,174,175],{"class":67,"line":74},[65,176,177],{},"CREATE INDEX idx_posts_author_id ON posts(author_id);\n",[65,179,180],{"class":67,"line":80},[65,181,182],{},"CREATE INDEX idx_posts_published_at ON posts(published_at DESC);\n",[15,184,185,186,189,190,193],{},"The ",[33,187,188],{},"DESC"," on ",[33,191,192],{},"published_at"," matters when your most common query orders by newest first. An index in the right sort order can avoid a sort operation.",[15,195,196,197,200,201,204],{},"Foreign keys always get indexes. ",[33,198,199],{},"posts.author_id"," should be indexed from day one — every ",[33,202,203],{},"JOIN users ON users.id = posts.author_id"," is a performance problem waiting to happen without it.",[22,206,208],{"id":207},"composite-indexes-column-order-matters","Composite Indexes: Column Order Matters",[15,210,211,212,215],{},"A composite index on ",[33,213,214],{},"(a, b)"," can answer queries for:",[118,217,218,223,228],{},[121,219,220],{},[33,221,222],{},"WHERE a = ?",[121,224,225],{},[33,226,227],{},"WHERE a = ? AND b = ?",[121,229,230],{},[33,231,232],{},"WHERE a = ? ORDER BY b",[15,234,235],{},"But NOT for:",[118,237,238],{},[121,239,240,243],{},[33,241,242],{},"WHERE b = ?"," (leading column must be present)",[56,245,247],{"className":58,"code":246,"language":60,"meta":61,"style":61},"-- This query benefits from a composite index on (user_id, status)\nSELECT * FROM orders\nWHERE user_id = $1\nAND status = 'pending'\nORDER BY created_at DESC;\n\nCREATE INDEX idx_orders_user_status ON orders(user_id, status);\n",[33,248,249,254,259,264,269,274,280],{"__ignoreMap":61},[65,250,251],{"class":67,"line":68},[65,252,253],{},"-- This query benefits from a composite index on (user_id, status)\n",[65,255,256],{"class":67,"line":74},[65,257,258],{},"SELECT * FROM orders\n",[65,260,261],{"class":67,"line":80},[65,262,263],{},"WHERE user_id = $1\n",[65,265,266],{"class":67,"line":86},[65,267,268],{},"AND status = 'pending'\n",[65,270,271],{"class":67,"line":92},[65,272,273],{},"ORDER BY created_at DESC;\n",[65,275,276],{"class":67,"line":98},[65,277,279],{"emptyLinePlaceholder":278},true,"\n",[65,281,282],{"class":67,"line":104},[65,283,284],{},"CREATE INDEX idx_orders_user_status ON orders(user_id, status);\n",[15,286,287],{},"The rule of thumb: put the most selective column first (the one that filters to the fewest rows), followed by columns used in equality conditions, followed by columns used in range conditions or ordering.",[15,289,290],{},"For the example above:",[118,292,293,300],{},[121,294,295,296,299],{},"If users typically have 100 orders, and only 5% are pending, ",[33,297,298],{},"status"," in position 2 filters from 100 to 5 rows.",[121,301,302,303,305,306,309,310,313],{},"Putting ",[33,304,298],{}," first would only help if you query ",[33,307,308],{},"WHERE status = 'pending'"," without a ",[33,311,312],{},"user_id"," filter.",[22,315,317],{"id":316},"partial-indexes","Partial Indexes",[15,319,320],{},"A partial index covers only the rows that match a condition. This is useful when you frequently query a subset of rows that is much smaller than the full table:",[56,322,324],{"className":58,"code":323,"language":60,"meta":61,"style":61},"-- Index only unread notifications (most notifications get marked read quickly)\nCREATE INDEX idx_notifications_unread\nON notifications(user_id, created_at)\nWHERE read = false;\n\n-- Index only active users\nCREATE INDEX idx_users_active_email\nON users(email)\nWHERE deleted_at IS NULL;\n",[33,325,326,331,336,341,346,350,355,360,365],{"__ignoreMap":61},[65,327,328],{"class":67,"line":68},[65,329,330],{},"-- Index only unread notifications (most notifications get marked read quickly)\n",[65,332,333],{"class":67,"line":74},[65,334,335],{},"CREATE INDEX idx_notifications_unread\n",[65,337,338],{"class":67,"line":80},[65,339,340],{},"ON notifications(user_id, created_at)\n",[65,342,343],{"class":67,"line":86},[65,344,345],{},"WHERE read = false;\n",[65,347,348],{"class":67,"line":92},[65,349,279],{"emptyLinePlaceholder":278},[65,351,352],{"class":67,"line":98},[65,353,354],{},"-- Index only active users\n",[65,356,357],{"class":67,"line":104},[65,358,359],{},"CREATE INDEX idx_users_active_email\n",[65,361,362],{"class":67,"line":110},[65,363,364],{},"ON users(email)\n",[65,366,368],{"class":67,"line":367},9,[65,369,370],{},"WHERE deleted_at IS NULL;\n",[15,372,373],{},"A partial index on active records is smaller and faster than a full-table index, and it exactly matches the query pattern.",[22,375,377],{"id":376},"covering-indexes","Covering Indexes",[15,379,380],{},"An Index Only Scan is the fastest possible plan — the database reads only the index and never touches the table. This happens when the index contains all the columns the query needs:",[56,382,384],{"className":58,"code":383,"language":60,"meta":61,"style":61},"-- Query that reads user list page\nSELECT id, name, email, created_at\nFROM users\nWHERE status = 'active'\nORDER BY created_at DESC;\n\n-- Covering index: includes all columns in the SELECT\nCREATE INDEX idx_users_active_covering\nON users(status, created_at DESC)\nINCLUDE (id, name, email);\n",[33,385,386,391,396,401,406,410,414,419,424,429],{"__ignoreMap":61},[65,387,388],{"class":67,"line":68},[65,389,390],{},"-- Query that reads user list page\n",[65,392,393],{"class":67,"line":74},[65,394,395],{},"SELECT id, name, email, created_at\n",[65,397,398],{"class":67,"line":80},[65,399,400],{},"FROM users\n",[65,402,403],{"class":67,"line":86},[65,404,405],{},"WHERE status = 'active'\n",[65,407,408],{"class":67,"line":92},[65,409,273],{},[65,411,412],{"class":67,"line":98},[65,413,279],{"emptyLinePlaceholder":278},[65,415,416],{"class":67,"line":104},[65,417,418],{},"-- Covering index: includes all columns in the SELECT\n",[65,420,421],{"class":67,"line":110},[65,422,423],{},"CREATE INDEX idx_users_active_covering\n",[65,425,426],{"class":67,"line":367},[65,427,428],{},"ON users(status, created_at DESC)\n",[65,430,432],{"class":67,"line":431},10,[65,433,434],{},"INCLUDE (id, name, email);\n",[15,436,185,437,440],{},[33,438,439],{},"INCLUDE"," clause adds non-key columns to the index. They cannot be used for filtering or ordering, but they are available for Index Only Scans. This is powerful for read-heavy queries on frequently accessed rows.",[22,442,444],{"id":443},"indexes-for-pattern-matching","Indexes for Pattern Matching",[15,446,447,448,451],{},"Standard B-tree indexes do not support prefix-insensitive pattern matching. ",[33,449,450],{},"LIKE '%term%'"," is always a sequential scan. For text search, you have options:",[15,453,454],{},[124,455,456,459],{},[33,457,458],{},"pg_trgm"," for fuzzy matching:",[56,461,463],{"className":58,"code":462,"language":60,"meta":61,"style":61},"CREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE INDEX idx_products_name_trgm\nON products USING gin(name gin_trgm_ops);\n\n-- Now this query can use the index\nSELECT * FROM products WHERE name ILIKE '%widget%';\n",[33,464,465,470,475,480,484,489],{"__ignoreMap":61},[65,466,467],{"class":67,"line":68},[65,468,469],{},"CREATE EXTENSION IF NOT EXISTS pg_trgm;\n",[65,471,472],{"class":67,"line":74},[65,473,474],{},"CREATE INDEX idx_products_name_trgm\n",[65,476,477],{"class":67,"line":80},[65,478,479],{},"ON products USING gin(name gin_trgm_ops);\n",[65,481,482],{"class":67,"line":86},[65,483,279],{"emptyLinePlaceholder":278},[65,485,486],{"class":67,"line":92},[65,487,488],{},"-- Now this query can use the index\n",[65,490,491],{"class":67,"line":98},[65,492,493],{},"SELECT * FROM products WHERE name ILIKE '%widget%';\n",[15,495,496],{},[124,497,498],{},"Full-text search indexes:",[56,500,502],{"className":58,"code":501,"language":60,"meta":61,"style":61},"CREATE INDEX idx_posts_search\nON posts USING gin(to_tsvector('english', title || ' ' || content));\n\nSELECT * FROM posts\nWHERE to_tsvector('english', title || ' ' || content)\n@@ to_tsquery('english', 'postgresql & indexing');\n",[33,503,504,509,514,518,523,528],{"__ignoreMap":61},[65,505,506],{"class":67,"line":68},[65,507,508],{},"CREATE INDEX idx_posts_search\n",[65,510,511],{"class":67,"line":74},[65,512,513],{},"ON posts USING gin(to_tsvector('english', title || ' ' || content));\n",[65,515,516],{"class":67,"line":80},[65,517,279],{"emptyLinePlaceholder":278},[65,519,520],{"class":67,"line":86},[65,521,522],{},"SELECT * FROM posts\n",[65,524,525],{"class":67,"line":92},[65,526,527],{},"WHERE to_tsvector('english', title || ' ' || content)\n",[65,529,530],{"class":67,"line":98},[65,531,532],{},"@@ to_tsquery('english', 'postgresql & indexing');\n",[15,534,535,538,539,542],{},[124,536,537],{},"For exact prefix matching"," (",[33,540,541],{},"LIKE 'term%'","), a standard B-tree index works:",[56,544,546],{"className":58,"code":545,"language":60,"meta":61,"style":61},"CREATE INDEX idx_users_name ON users(name);\n-- LIKE 'James%' uses the index; LIKE '%James%' does not\n",[33,547,548,553],{"__ignoreMap":61},[65,549,550],{"class":67,"line":68},[65,551,552],{},"CREATE INDEX idx_users_name ON users(name);\n",[65,554,555],{"class":67,"line":74},[65,556,557],{},"-- LIKE 'James%' uses the index; LIKE '%James%' does not\n",[22,559,561],{"id":560},"json-and-jsonb-indexes","JSON and JSONB Indexes",[15,563,564],{},"For JSONB columns, GIN indexes enable querying nested fields:",[56,566,568],{"className":58,"code":567,"language":60,"meta":61,"style":61},"-- Index all keys in a JSONB column\nCREATE INDEX idx_metadata ON items USING gin(metadata);\n\n-- Or index a specific path for better performance\nCREATE INDEX idx_metadata_category\nON items((metadata->>'category'));\n\n-- Query that uses the expression index\nSELECT * FROM items WHERE metadata->>'category' = 'electronics';\n",[33,569,570,575,580,584,589,594,599,603,608],{"__ignoreMap":61},[65,571,572],{"class":67,"line":68},[65,573,574],{},"-- Index all keys in a JSONB column\n",[65,576,577],{"class":67,"line":74},[65,578,579],{},"CREATE INDEX idx_metadata ON items USING gin(metadata);\n",[65,581,582],{"class":67,"line":80},[65,583,279],{"emptyLinePlaceholder":278},[65,585,586],{"class":67,"line":86},[65,587,588],{},"-- Or index a specific path for better performance\n",[65,590,591],{"class":67,"line":92},[65,592,593],{},"CREATE INDEX idx_metadata_category\n",[65,595,596],{"class":67,"line":98},[65,597,598],{},"ON items((metadata->>'category'));\n",[65,600,601],{"class":67,"line":104},[65,602,279],{"emptyLinePlaceholder":278},[65,604,605],{"class":67,"line":110},[65,606,607],{},"-- Query that uses the expression index\n",[65,609,610],{"class":67,"line":367},[65,611,612],{},"SELECT * FROM items WHERE metadata->>'category' = 'electronics';\n",[22,614,616],{"id":615},"when-not-to-add-an-index","When NOT to Add an Index",[15,618,619],{},"Indexes are not free. Every index:",[118,621,622,625,628],{},[121,623,624],{},"Takes disk space",[121,626,627],{},"Slows down INSERT, UPDATE, and DELETE operations (the index must be updated)",[121,629,630],{},"Must be maintained by VACUUM and autovacuum",[15,632,633],{},"Do not index:",[118,635,636,639,642],{},[121,637,638],{},"Columns with very low cardinality (boolean columns, status columns with 2-3 values)",[121,640,641],{},"Columns that are never queried in WHERE, JOIN, or ORDER BY",[121,643,644],{},"Small tables (under ~1,000 rows) where sequential scans are faster",[22,646,648],{"id":647},"detecting-missing-indexes-in-production","Detecting Missing Indexes in Production",[15,650,651,652,655],{},"PostgreSQL tracks sequential scans on each table. Query ",[33,653,654],{},"pg_stat_user_tables"," to find tables with many sequential scans:",[56,657,659],{"className":58,"code":658,"language":60,"meta":61,"style":61},"SELECT\n schemaname,\n tablename,\n seq_scan,\n seq_tup_read,\n idx_scan,\n n_live_tup\nFROM pg_stat_user_tables\nWHERE seq_scan > 100\nAND n_live_tup > 10000\nORDER BY seq_scan DESC;\n",[33,660,661,666,671,676,681,686,691,696,701,706,711],{"__ignoreMap":61},[65,662,663],{"class":67,"line":68},[65,664,665],{},"SELECT\n",[65,667,668],{"class":67,"line":74},[65,669,670],{}," schemaname,\n",[65,672,673],{"class":67,"line":80},[65,674,675],{}," tablename,\n",[65,677,678],{"class":67,"line":86},[65,679,680],{}," seq_scan,\n",[65,682,683],{"class":67,"line":92},[65,684,685],{}," seq_tup_read,\n",[65,687,688],{"class":67,"line":98},[65,689,690],{}," idx_scan,\n",[65,692,693],{"class":67,"line":104},[65,694,695],{}," n_live_tup\n",[65,697,698],{"class":67,"line":110},[65,699,700],{},"FROM pg_stat_user_tables\n",[65,702,703],{"class":67,"line":367},[65,704,705],{},"WHERE seq_scan > 100\n",[65,707,708],{"class":67,"line":431},[65,709,710],{},"AND n_live_tup > 10000\n",[65,712,714],{"class":67,"line":713},11,[65,715,716],{},"ORDER BY seq_scan DESC;\n",[15,718,719,720,723],{},"Tables with many sequential scans and many rows are your index candidates. Cross-reference with ",[33,721,722],{},"pg_stat_statements"," (if enabled) to find the specific queries driving those scans.",[15,725,726],{},"Enable slow query logging to catch queries that take over 100ms:",[56,728,730],{"className":58,"code":729,"language":60,"meta":61,"style":61},"-- In postgresql.conf\nlog_min_duration_statement = 100 -- log queries over 100ms\n",[33,731,732,737],{"__ignoreMap":61},[65,733,734],{"class":67,"line":68},[65,735,736],{},"-- In postgresql.conf\n",[65,738,739],{"class":67,"line":74},[65,740,741],{},"log_min_duration_statement = 100 -- log queries over 100ms\n",[15,743,744],{},"Indexing is not a one-time activity. As your application grows and query patterns change, revisit your index strategy. The indexes that served you at 10,000 rows need review at 10,000,000 rows.",[746,747],"hr",{},[15,749,750,751,758],{},"Dealing with slow database queries or want help designing an indexing strategy for a growing application? This is exactly the kind of problem I help with. Book a call: ",[752,753,757],"a",{"href":754,"rel":755},"https://calendly.com/jamesrossjr",[756],"nofollow","calendly.com/jamesrossjr",".",[746,760],{},[22,762,764],{"id":763},"keep-reading","Keep Reading",[118,766,767,773,779,785],{},[121,768,769],{},[752,770,772],{"href":771},"/blog/database-backup-strategies","Database Backup Strategies for Production: The Ones That Actually Work",[121,774,775],{},[752,776,778],{"href":777},"/blog/database-connection-pooling","Database Connection Pooling: Why It Matters and How to Configure It",[121,780,781],{},[752,782,784],{"href":783},"/blog/database-query-performance","Database Query Performance: Finding and Fixing the Slow Ones",[121,786,787],{},[752,788,790],{"href":789},"/blog/database-migrations-guide","Database Migrations in Production: Zero-Downtime Strategies",[792,793,794],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":61,"searchDepth":80,"depth":80,"links":796},[797,798,799,800,801,802,803,804,805,806,807],{"id":24,"depth":74,"text":25},{"id":46,"depth":74,"text":47},{"id":158,"depth":74,"text":159},{"id":207,"depth":74,"text":208},{"id":316,"depth":74,"text":317},{"id":376,"depth":74,"text":377},{"id":443,"depth":74,"text":444},{"id":560,"depth":74,"text":561},{"id":615,"depth":74,"text":616},{"id":647,"depth":74,"text":648},{"id":763,"depth":74,"text":764},"Engineering","2026-03-03","A practical guide to database indexing for application developers — B-tree indexes, composite indexes, partial indexes, covering indexes, and how to read query plans.","md",false,null,[815,816],"database indexing","database performance",{},"/blog/database-indexing-strategies",{"title":7,"description":810},"blog/database-indexing-strategies",[822,823,824],"Database","PostgreSQL","Performance","XBBP-_9hcGxeLkFZOekm9CUd0afFv3nzZ90OuB2nG_8",{"id":827,"title":790,"author":828,"body":829,"category":808,"date":809,"description":1645,"extension":811,"featured":812,"image":813,"keywords":1646,"meta":1649,"navigation":278,"path":789,"readTime":104,"seo":1650,"stem":1651,"tags":1652,"__hash__":1654},"blog/blog/database-migrations-guide.md",{"name":9,"bio":10},{"type":12,"value":830,"toc":1632},[831,834,837,841,844,847,850,854,859,890,895,912,916,919,925,931,937,942,951,956,971,982,1053,1062,1074,1094,1097,1101,1106,1115,1120,1223,1228,1237,1240,1255,1259,1266,1272,1300,1305,1308,1361,1365,1368,1463,1466,1470,1473,1476,1479,1490,1493,1497,1500,1511,1518,1596,1599,1601,1607,1609,1611,1629],[15,832,833],{},"Database migrations are where confident developers become nervous. Get them wrong and you have production downtime, data corruption, or a rollback that takes longer than the original migration. Get them right and they are invisible — users never know they happened.",[15,835,836],{},"The difference between getting them right and wrong comes down to understanding which operations are safe while the application is running and which are not.",[22,838,840],{"id":839},"the-core-problem","The Core Problem",[15,842,843],{},"When you deploy a migration, you have a window where the old application code and the new application code may both be running simultaneously. Old code runs during deployment while new instances come up. Both versions must be able to work with whatever state the database is in.",[15,845,846],{},"This constraint rules out some common migration patterns. Adding a NOT NULL column with no default? The old application code does not know to set this value — it will start failing the moment the migration runs. Renaming a column? Old code looks for the old name, which is gone.",[15,848,849],{},"The solution is the expand-contract pattern: make database changes that are backward-compatible, deploy the new application code, then remove compatibility shims.",[22,851,853],{"id":852},"safe-vs-unsafe-schema-changes","Safe vs Unsafe Schema Changes",[15,855,856],{},[124,857,858],{},"Safe operations (can run while application is running):",[118,860,861,864,867,870,877,887],{},[121,862,863],{},"Adding a nullable column",[121,865,866],{},"Adding a column with a default value",[121,868,869],{},"Adding a new table",[121,871,872,873,876],{},"Adding an index (with ",[33,874,875],{},"CONCURRENTLY",")",[121,878,879,880,883,884],{},"Adding a foreign key constraint with ",[33,881,882],{},"NOT VALID"," then ",[33,885,886],{},"VALIDATE CONSTRAINT",[121,888,889],{},"Dropping a column the application no longer references",[15,891,892],{},[124,893,894],{},"Unsafe operations (require downtime or multi-step process):",[118,896,897,900,903,906,909],{},[121,898,899],{},"Adding a NOT NULL column without a default (PostgreSQL \u003C 14)",[121,901,902],{},"Renaming a column",[121,904,905],{},"Changing a column's type",[121,907,908],{},"Adding a unique constraint (takes a full table lock)",[121,910,911],{},"Dropping a column the application currently reads",[22,913,915],{"id":914},"the-expand-contract-pattern","The Expand-Contract Pattern",[15,917,918],{},"Every dangerous schema change becomes safe when decomposed into three phases:",[15,920,921,924],{},[124,922,923],{},"Expand:"," Add the new structure while keeping the old. Both old and new code can run.",[15,926,927,930],{},[124,928,929],{},"Migrate:"," Backfill data, update application code to use the new structure.",[15,932,933,936],{},[124,934,935],{},"Contract:"," Remove the old structure once all code uses the new version.",[938,939,941],"h3",{"id":940},"example-renaming-a-column","Example: Renaming a Column",[15,943,944,947,948],{},[33,945,946],{},"users.full_name"," → ",[33,949,950],{},"users.name",[15,952,953],{},[124,954,955],{},"Phase 1 — Expand (migration):",[56,957,959],{"className":58,"code":958,"language":60,"meta":61,"style":61},"ALTER TABLE users ADD COLUMN name TEXT;\nUPDATE users SET name = full_name;\n",[33,960,961,966],{"__ignoreMap":61},[65,962,963],{"class":67,"line":68},[65,964,965],{},"ALTER TABLE users ADD COLUMN name TEXT;\n",[65,967,968],{"class":67,"line":74},[65,969,970],{},"UPDATE users SET name = full_name;\n",[15,972,973,974,977,978,981],{},"Application code continues to write ",[33,975,976],{},"full_name",". A trigger keeps ",[33,979,980],{},"name"," in sync:",[56,983,985],{"className":58,"code":984,"language":60,"meta":61,"style":61},"CREATE OR REPLACE FUNCTION sync_user_name()\nRETURNS TRIGGER AS $$\nBEGIN\n IF TG_OP = 'INSERT' OR NEW.full_name IS DISTINCT FROM OLD.full_name THEN\n NEW.name := NEW.full_name;\n END IF;\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER sync_user_name_trigger\nBEFORE INSERT OR UPDATE ON users\nFOR EACH ROW EXECUTE FUNCTION sync_user_name();\n",[33,986,987,992,997,1002,1007,1012,1017,1022,1027,1032,1036,1041,1047],{"__ignoreMap":61},[65,988,989],{"class":67,"line":68},[65,990,991],{},"CREATE OR REPLACE FUNCTION sync_user_name()\n",[65,993,994],{"class":67,"line":74},[65,995,996],{},"RETURNS TRIGGER AS $$\n",[65,998,999],{"class":67,"line":80},[65,1000,1001],{},"BEGIN\n",[65,1003,1004],{"class":67,"line":86},[65,1005,1006],{}," IF TG_OP = 'INSERT' OR NEW.full_name IS DISTINCT FROM OLD.full_name THEN\n",[65,1008,1009],{"class":67,"line":92},[65,1010,1011],{}," NEW.name := NEW.full_name;\n",[65,1013,1014],{"class":67,"line":98},[65,1015,1016],{}," END IF;\n",[65,1018,1019],{"class":67,"line":104},[65,1020,1021],{}," RETURN NEW;\n",[65,1023,1024],{"class":67,"line":110},[65,1025,1026],{},"END;\n",[65,1028,1029],{"class":67,"line":367},[65,1030,1031],{},"$$ LANGUAGE plpgsql;\n",[65,1033,1034],{"class":67,"line":431},[65,1035,279],{"emptyLinePlaceholder":278},[65,1037,1038],{"class":67,"line":713},[65,1039,1040],{},"CREATE TRIGGER sync_user_name_trigger\n",[65,1042,1044],{"class":67,"line":1043},12,[65,1045,1046],{},"BEFORE INSERT OR UPDATE ON users\n",[65,1048,1050],{"class":67,"line":1049},13,[65,1051,1052],{},"FOR EACH ROW EXECUTE FUNCTION sync_user_name();\n",[15,1054,1055,1058,1059,1061],{},[124,1056,1057],{},"Phase 2 — Migrate:"," Deploy application code that reads from ",[33,1060,980],{}," and writes to both. Verify everything works.",[15,1063,1064,1067,1068,1070,1071,1073],{},[124,1065,1066],{},"Phase 3 — Contract:"," Deploy code that reads and writes only ",[33,1069,980],{},". Drop ",[33,1072,976],{}," and the trigger.",[56,1075,1077],{"className":58,"code":1076,"language":60,"meta":61,"style":61},"DROP TRIGGER sync_user_name_trigger ON users;\nDROP FUNCTION sync_user_name();\nALTER TABLE users DROP COLUMN full_name;\n",[33,1078,1079,1084,1089],{"__ignoreMap":61},[65,1080,1081],{"class":67,"line":68},[65,1082,1083],{},"DROP TRIGGER sync_user_name_trigger ON users;\n",[65,1085,1086],{"class":67,"line":74},[65,1087,1088],{},"DROP FUNCTION sync_user_name();\n",[65,1090,1091],{"class":67,"line":80},[65,1092,1093],{},"ALTER TABLE users DROP COLUMN full_name;\n",[15,1095,1096],{},"This process takes more time than a rename in a maintenance window, but it never requires downtime.",[938,1098,1100],{"id":1099},"example-adding-a-not-null-column","Example: Adding a NOT NULL Column",[15,1102,1103],{},[124,1104,1105],{},"Phase 1 — Add as nullable:",[56,1107,1109],{"className":58,"code":1108,"language":60,"meta":61,"style":61},"ALTER TABLE orders ADD COLUMN customer_notes TEXT;\n",[33,1110,1111],{"__ignoreMap":61},[65,1112,1113],{"class":67,"line":68},[65,1114,1108],{},[15,1116,1117],{},[124,1118,1119],{},"Phase 2 — Backfill (for existing rows):",[56,1121,1123],{"className":58,"code":1122,"language":60,"meta":61,"style":61},"-- Backfill in batches to avoid locking\nDO $$\nDECLARE\n batch_size INT := 1000;\n last_id BIGINT := 0;\n max_id BIGINT;\nBEGIN\n SELECT MAX(id) INTO max_id FROM orders;\n\n WHILE last_id \u003C max_id LOOP\n UPDATE orders\n SET customer_notes = ''\n WHERE id > last_id AND id \u003C= last_id + batch_size\n AND customer_notes IS NULL;\n\n last_id := last_id + batch_size;\n PERFORM pg_sleep(0.01); -- Brief pause to reduce I/O pressure\n END LOOP;\nEND $$;\n",[33,1124,1125,1130,1135,1140,1145,1150,1155,1159,1164,1168,1173,1178,1183,1188,1194,1199,1205,1211,1217],{"__ignoreMap":61},[65,1126,1127],{"class":67,"line":68},[65,1128,1129],{},"-- Backfill in batches to avoid locking\n",[65,1131,1132],{"class":67,"line":74},[65,1133,1134],{},"DO $$\n",[65,1136,1137],{"class":67,"line":80},[65,1138,1139],{},"DECLARE\n",[65,1141,1142],{"class":67,"line":86},[65,1143,1144],{}," batch_size INT := 1000;\n",[65,1146,1147],{"class":67,"line":92},[65,1148,1149],{}," last_id BIGINT := 0;\n",[65,1151,1152],{"class":67,"line":98},[65,1153,1154],{}," max_id BIGINT;\n",[65,1156,1157],{"class":67,"line":104},[65,1158,1001],{},[65,1160,1161],{"class":67,"line":110},[65,1162,1163],{}," SELECT MAX(id) INTO max_id FROM orders;\n",[65,1165,1166],{"class":67,"line":367},[65,1167,279],{"emptyLinePlaceholder":278},[65,1169,1170],{"class":67,"line":431},[65,1171,1172],{}," WHILE last_id \u003C max_id LOOP\n",[65,1174,1175],{"class":67,"line":713},[65,1176,1177],{}," UPDATE orders\n",[65,1179,1180],{"class":67,"line":1043},[65,1181,1182],{}," SET customer_notes = ''\n",[65,1184,1185],{"class":67,"line":1049},[65,1186,1187],{}," WHERE id > last_id AND id \u003C= last_id + batch_size\n",[65,1189,1191],{"class":67,"line":1190},14,[65,1192,1193],{}," AND customer_notes IS NULL;\n",[65,1195,1197],{"class":67,"line":1196},15,[65,1198,279],{"emptyLinePlaceholder":278},[65,1200,1202],{"class":67,"line":1201},16,[65,1203,1204],{}," last_id := last_id + batch_size;\n",[65,1206,1208],{"class":67,"line":1207},17,[65,1209,1210],{}," PERFORM pg_sleep(0.01); -- Brief pause to reduce I/O pressure\n",[65,1212,1214],{"class":67,"line":1213},18,[65,1215,1216],{}," END LOOP;\n",[65,1218,1220],{"class":67,"line":1219},19,[65,1221,1222],{},"END $$;\n",[15,1224,1225],{},[124,1226,1227],{},"Phase 3 — Add NOT NULL constraint:",[56,1229,1231],{"className":58,"code":1230,"language":60,"meta":61,"style":61},"ALTER TABLE orders ALTER COLUMN customer_notes SET NOT NULL;\n",[33,1232,1233],{"__ignoreMap":61},[65,1234,1235],{"class":67,"line":68},[65,1236,1230],{},[15,1238,1239],{},"In PostgreSQL 14+, adding a NOT NULL column with a constant default is safe and instant (the default is stored in the catalog, not written to every row). This eliminates the need for the expand-contract pattern in simple cases:",[56,1241,1243],{"className":58,"code":1242,"language":60,"meta":61,"style":61},"-- Safe in PostgreSQL 14+: instant, no table rewrite\nALTER TABLE orders ADD COLUMN customer_notes TEXT NOT NULL DEFAULT '';\n",[33,1244,1245,1250],{"__ignoreMap":61},[65,1246,1247],{"class":67,"line":68},[65,1248,1249],{},"-- Safe in PostgreSQL 14+: instant, no table rewrite\n",[65,1251,1252],{"class":67,"line":74},[65,1253,1254],{},"ALTER TABLE orders ADD COLUMN customer_notes TEXT NOT NULL DEFAULT '';\n",[22,1256,1258],{"id":1257},"adding-indexes-without-locking","Adding Indexes Without Locking",[15,1260,1261,1262,1265],{},"A standard ",[33,1263,1264],{},"CREATE INDEX"," takes an access share lock that blocks writes for the duration. On a large table, this can take hours and cause production downtime.",[15,1267,1268,1269,1271],{},"Always use ",[33,1270,875],{}," in production:",[56,1273,1275],{"className":58,"code":1274,"language":60,"meta":61,"style":61},"-- This blocks for the entire duration (bad for production)\nCREATE INDEX idx_posts_author_id ON posts(author_id);\n\n-- This builds the index without blocking writes (good for production)\nCREATE INDEX CONCURRENTLY idx_posts_author_id ON posts(author_id);\n",[33,1276,1277,1282,1286,1290,1295],{"__ignoreMap":61},[65,1278,1279],{"class":67,"line":68},[65,1280,1281],{},"-- This blocks for the entire duration (bad for production)\n",[65,1283,1284],{"class":67,"line":74},[65,1285,177],{},[65,1287,1288],{"class":67,"line":80},[65,1289,279],{"emptyLinePlaceholder":278},[65,1291,1292],{"class":67,"line":86},[65,1293,1294],{},"-- This builds the index without blocking writes (good for production)\n",[65,1296,1297],{"class":67,"line":92},[65,1298,1299],{},"CREATE INDEX CONCURRENTLY idx_posts_author_id ON posts(author_id);\n",[15,1301,1302,1304],{},[33,1303,875],{}," takes longer because it makes two passes and can only proceed when there are no conflicting locks. It also cannot run inside a transaction. But it does not block your application.",[15,1306,1307],{},"If a concurrent index build fails partway through, it leaves an invalid index that must be dropped before retrying:",[56,1309,1311],{"className":58,"code":1310,"language":60,"meta":61,"style":61},"-- Check for invalid indexes\nSELECT schemaname, tablename, indexname, indisvalid\nFROM pg_indexes\nJOIN pg_class ON pg_class.relname = indexname\nJOIN pg_index ON pg_index.indexrelid = pg_class.oid\nWHERE NOT pg_index.indisvalid;\n\n-- Drop and recreate if found\nDROP INDEX CONCURRENTLY idx_posts_author_id;\nCREATE INDEX CONCURRENTLY idx_posts_author_id ON posts(author_id);\n",[33,1312,1313,1318,1323,1328,1333,1338,1343,1347,1352,1357],{"__ignoreMap":61},[65,1314,1315],{"class":67,"line":68},[65,1316,1317],{},"-- Check for invalid indexes\n",[65,1319,1320],{"class":67,"line":74},[65,1321,1322],{},"SELECT schemaname, tablename, indexname, indisvalid\n",[65,1324,1325],{"class":67,"line":80},[65,1326,1327],{},"FROM pg_indexes\n",[65,1329,1330],{"class":67,"line":86},[65,1331,1332],{},"JOIN pg_class ON pg_class.relname = indexname\n",[65,1334,1335],{"class":67,"line":92},[65,1336,1337],{},"JOIN pg_index ON pg_index.indexrelid = pg_class.oid\n",[65,1339,1340],{"class":67,"line":98},[65,1341,1342],{},"WHERE NOT pg_index.indisvalid;\n",[65,1344,1345],{"class":67,"line":104},[65,1346,279],{"emptyLinePlaceholder":278},[65,1348,1349],{"class":67,"line":110},[65,1350,1351],{},"-- Drop and recreate if found\n",[65,1353,1354],{"class":67,"line":367},[65,1355,1356],{},"DROP INDEX CONCURRENTLY idx_posts_author_id;\n",[65,1358,1359],{"class":67,"line":431},[65,1360,1299],{},[22,1362,1364],{"id":1363},"migration-management-in-cicd","Migration Management in CI/CD",[15,1366,1367],{},"Structure your deployment pipeline to run migrations before deploying new application code:",[56,1369,1373],{"className":1370,"code":1371,"language":1372,"meta":61,"style":61},"language-yaml shiki shiki-themes github-dark","# .github/workflows/deploy.yml\ndeploy:\n steps:\n - name: Run database migrations\n run: prisma migrate deploy\n env:\n DATABASE_URL: ${{ secrets.DATABASE_URL }}\n\n - name: Deploy application\n run: kubectl rollout restart deployment/api\n","yaml",[33,1374,1375,1381,1391,1398,1412,1422,1429,1439,1443,1454],{"__ignoreMap":61},[65,1376,1377],{"class":67,"line":68},[65,1378,1380],{"class":1379},"sAwPA","# .github/workflows/deploy.yml\n",[65,1382,1383,1387],{"class":67,"line":74},[65,1384,1386],{"class":1385},"s4JwU","deploy",[65,1388,1390],{"class":1389},"s95oV",":\n",[65,1392,1393,1396],{"class":67,"line":80},[65,1394,1395],{"class":1385}," steps",[65,1397,1390],{"class":1389},[65,1399,1400,1403,1405,1408],{"class":67,"line":86},[65,1401,1402],{"class":1389}," - ",[65,1404,980],{"class":1385},[65,1406,1407],{"class":1389},": ",[65,1409,1411],{"class":1410},"sU2Wk","Run database migrations\n",[65,1413,1414,1417,1419],{"class":67,"line":92},[65,1415,1416],{"class":1385}," run",[65,1418,1407],{"class":1389},[65,1420,1421],{"class":1410},"prisma migrate deploy\n",[65,1423,1424,1427],{"class":67,"line":98},[65,1425,1426],{"class":1385}," env",[65,1428,1390],{"class":1389},[65,1430,1431,1434,1436],{"class":67,"line":104},[65,1432,1433],{"class":1385}," DATABASE_URL",[65,1435,1407],{"class":1389},[65,1437,1438],{"class":1410},"${{ secrets.DATABASE_URL }}\n",[65,1440,1441],{"class":67,"line":110},[65,1442,279],{"emptyLinePlaceholder":278},[65,1444,1445,1447,1449,1451],{"class":67,"line":367},[65,1446,1402],{"class":1389},[65,1448,980],{"class":1385},[65,1450,1407],{"class":1389},[65,1452,1453],{"class":1410},"Deploy application\n",[65,1455,1456,1458,1460],{"class":67,"line":431},[65,1457,1416],{"class":1385},[65,1459,1407],{"class":1389},[65,1461,1462],{"class":1410},"kubectl rollout restart deployment/api\n",[15,1464,1465],{},"This order matters. New application code must be able to work with the pre-migration schema (during the migration window) and the post-migration schema. Design your application code to be backward-compatible with the old schema until all instances have updated.",[22,1467,1469],{"id":1468},"rollback-planning","Rollback Planning",[15,1471,1472],{},"Not every migration is reversible. Before running a migration in production, know the answer to \"what is my rollback plan?\"",[15,1474,1475],{},"For additive changes (new columns, new tables): rollback is trivial — drop what was added.",[15,1477,1478],{},"For destructive changes (dropping columns, type changes): rollback requires either:",[118,1480,1481,1484,1487],{},[121,1482,1483],{},"A database snapshot from before the migration",[121,1485,1486],{},"A reverse migration that restores the structure (may lose data added after the forward migration)",[121,1488,1489],{},"The expand-contract pattern which avoids the need for rollback",[15,1491,1492],{},"Always take a database snapshot before running a major migration. Most managed databases make this trivial. The 10 minutes to take and verify a snapshot is much cheaper than the hours spent recovering from a failed migration without one.",[22,1494,1496],{"id":1495},"testing-migrations","Testing Migrations",[15,1498,1499],{},"Test migrations in a staging environment that matches production in:",[118,1501,1502,1505,1508],{},[121,1503,1504],{},"Row count (not just schema)",[121,1506,1507],{},"Index configuration",[121,1509,1510],{},"PostgreSQL version",[15,1512,1513,1514,1517],{},"A migration that takes 2 seconds on a 1,000-row test database might take 45 minutes on a 50-million-row production database. Always run ",[33,1515,1516],{},"EXPLAIN"," and estimate time from your staging data volume before running in production.",[56,1519,1523],{"className":1520,"code":1521,"language":1522,"meta":61,"style":61},"language-bash shiki shiki-themes github-dark","# Restore production data to staging (anonymized)\npg_dump $PRODUCTION_URL | pg_anonymizer | psql $STAGING_URL\n\n# Test the migration\npsql $STAGING_URL -f migration.sql\n\n# Measure execution time on staging data\ntime psql $STAGING_URL -f migration.sql\n","bash",[33,1524,1525,1530,1555,1559,1564,1579,1583,1588],{"__ignoreMap":61},[65,1526,1527],{"class":67,"line":68},[65,1528,1529],{"class":1379},"# Restore production data to staging (anonymized)\n",[65,1531,1532,1536,1539,1543,1546,1549,1552],{"class":67,"line":74},[65,1533,1535],{"class":1534},"svObZ","pg_dump",[65,1537,1538],{"class":1389}," $PRODUCTION_URL ",[65,1540,1542],{"class":1541},"snl16","|",[65,1544,1545],{"class":1534}," pg_anonymizer",[65,1547,1548],{"class":1541}," |",[65,1550,1551],{"class":1534}," psql",[65,1553,1554],{"class":1389}," $STAGING_URL\n",[65,1556,1557],{"class":67,"line":80},[65,1558,279],{"emptyLinePlaceholder":278},[65,1560,1561],{"class":67,"line":86},[65,1562,1563],{"class":1379},"# Test the migration\n",[65,1565,1566,1569,1572,1576],{"class":67,"line":92},[65,1567,1568],{"class":1534},"psql",[65,1570,1571],{"class":1389}," $STAGING_URL ",[65,1573,1575],{"class":1574},"sDLfK","-f",[65,1577,1578],{"class":1410}," migration.sql\n",[65,1580,1581],{"class":67,"line":98},[65,1582,279],{"emptyLinePlaceholder":278},[65,1584,1585],{"class":67,"line":104},[65,1586,1587],{"class":1379},"# Measure execution time on staging data\n",[65,1589,1590,1593],{"class":67,"line":110},[65,1591,1592],{"class":1541},"time",[65,1594,1595],{"class":1389}," psql $STAGING_URL -f migration.sql\n",[15,1597,1598],{},"Database migrations are not glamorous work, but they are consequential. A disciplined approach — expand-contract for dangerous changes, concurrent index builds, mandatory staging testing, pre-migration snapshots — keeps what should be invisible changes from becoming incidents.",[746,1600],{},[15,1602,1603,1604,758],{},"Planning a complex database migration or need help designing a zero-downtime migration strategy? Book a call and let's think through the approach together: ",[752,1605,757],{"href":754,"rel":1606},[756],[746,1608],{},[22,1610,764],{"id":763},[118,1612,1613,1617,1621,1625],{},[121,1614,1615],{},[752,1616,772],{"href":771},[121,1618,1619],{},[752,1620,7],{"href":818},[121,1622,1623],{},[752,1624,778],{"href":777},[121,1626,1627],{},[752,1628,784],{"href":783},[792,1630,1631],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":61,"searchDepth":80,"depth":80,"links":1633},[1634,1635,1636,1640,1641,1642,1643,1644],{"id":839,"depth":74,"text":840},{"id":852,"depth":74,"text":853},{"id":914,"depth":74,"text":915,"children":1637},[1638,1639],{"id":940,"depth":80,"text":941},{"id":1099,"depth":80,"text":1100},{"id":1257,"depth":74,"text":1258},{"id":1363,"depth":74,"text":1364},{"id":1468,"depth":74,"text":1469},{"id":1495,"depth":74,"text":1496},{"id":763,"depth":74,"text":764},"How to run database migrations in production without downtime — expand-contract patterns, safe column changes, large table strategies, and rollback plans that actually work.",[1647,1648],"database migrations","production database",{},{"title":790,"description":1645},"blog/database-migrations-guide",[822,1653,823],"Migrations","vFXAKuu2BoymdLNj0NmW8yGQlfnvh2Mcovca6sKS_cQ",{"id":1656,"title":784,"author":1657,"body":1658,"category":808,"date":809,"description":2465,"extension":811,"featured":812,"image":813,"keywords":2466,"meta":2469,"navigation":278,"path":783,"readTime":104,"seo":2470,"stem":2471,"tags":2472,"__hash__":2473},"blog/blog/database-query-performance.md",{"name":9,"bio":10},{"type":12,"value":1659,"toc":2456},[1660,1664,1667,1670,1673,1675,1679,1688,1712,1715,1721,1724,1867,1870,1876,1879,1992,2003,2005,2009,2014,2056,2059,2065,2071,2077,2087,2093,2095,2099,2102,2107,2136,2141,2170,2175,2204,2209,2237,2247,2249,2253,2262,2267,2296,2301,2330,2341,2351,2361,2386,2388,2392,2395,2422,2424,2431,2433,2435,2453],[22,1661,1663],{"id":1662},"most-api-performance-problems-are-database-problems","Most API Performance Problems Are Database Problems",[15,1665,1666],{},"When an API endpoint is slow, the query profiling usually tells the same story: one or two database queries account for 80-95% of the response time. Everything else — network, serialization, business logic — is noise by comparison. This means that optimizing your database queries is typically the highest-leverage performance work available to you.",[15,1668,1669],{},"The challenge is that slow queries often hide. They don't throw exceptions. They don't fail visibly. They just make your users wait, and without active monitoring, you may not know which queries are slow or why.",[15,1671,1672],{},"This article walks through the systematic approach to finding slow queries and the common patterns that cause them.",[746,1674],{},[22,1676,1678],{"id":1677},"finding-slow-queries","Finding Slow Queries",[15,1680,1681,1684,1685,1687],{},[124,1682,1683],{},"Enable slow query logging."," PostgreSQL's ",[33,1686,722],{}," extension tracks query statistics including total execution time, number of calls, and average duration. Enable it in your database configuration and query the view regularly:",[56,1689,1691],{"className":58,"code":1690,"language":60,"meta":61,"style":61},"SELECT query, calls, total_exec_time, mean_exec_time, stddev_exec_time\nFROM pg_stat_statements\nORDER BY mean_exec_time DESC\nLIMIT 20;\n",[33,1692,1693,1698,1703,1708],{"__ignoreMap":61},[65,1694,1695],{"class":67,"line":68},[65,1696,1697],{},"SELECT query, calls, total_exec_time, mean_exec_time, stddev_exec_time\n",[65,1699,1700],{"class":67,"line":74},[65,1701,1702],{},"FROM pg_stat_statements\n",[65,1704,1705],{"class":67,"line":80},[65,1706,1707],{},"ORDER BY mean_exec_time DESC\n",[65,1709,1710],{"class":67,"line":86},[65,1711,113],{},[15,1713,1714],{},"This shows you the 20 slowest queries by average execution time. Run this query after your application has been under normal load and you'll immediately see where the time is going.",[15,1716,1717,1720],{},[124,1718,1719],{},"Application-level query timing."," In your ORM or database client, enable query logging with timing:",[15,1722,1723],{},"For Prisma:",[56,1725,1729],{"className":1726,"code":1727,"language":1728,"meta":61,"style":61},"language-typescript shiki shiki-themes github-dark","const prisma = new PrismaClient({\n log: [{ emit: 'event', level: 'query' }],\n})\nprisma.$on('query', (e) => {\n if (e.duration > 100) { // log queries over 100ms\n console.warn(`Slow query (${e.duration}ms): ${e.query}`)\n }\n})\n","typescript",[33,1730,1731,1751,1768,1773,1802,1822,1858,1863],{"__ignoreMap":61},[65,1732,1733,1736,1739,1742,1745,1748],{"class":67,"line":68},[65,1734,1735],{"class":1541},"const",[65,1737,1738],{"class":1574}," prisma",[65,1740,1741],{"class":1541}," =",[65,1743,1744],{"class":1541}," new",[65,1746,1747],{"class":1534}," PrismaClient",[65,1749,1750],{"class":1389},"({\n",[65,1752,1753,1756,1759,1762,1765],{"class":67,"line":74},[65,1754,1755],{"class":1389}," log: [{ emit: ",[65,1757,1758],{"class":1410},"'event'",[65,1760,1761],{"class":1389},", level: ",[65,1763,1764],{"class":1410},"'query'",[65,1766,1767],{"class":1389}," }],\n",[65,1769,1770],{"class":67,"line":80},[65,1771,1772],{"class":1389},"})\n",[65,1774,1775,1778,1781,1784,1786,1789,1793,1796,1799],{"class":67,"line":86},[65,1776,1777],{"class":1389},"prisma.",[65,1779,1780],{"class":1534},"$on",[65,1782,1783],{"class":1389},"(",[65,1785,1764],{"class":1410},[65,1787,1788],{"class":1389},", (",[65,1790,1792],{"class":1791},"s9osk","e",[65,1794,1795],{"class":1389},") ",[65,1797,1798],{"class":1541},"=>",[65,1800,1801],{"class":1389}," {\n",[65,1803,1804,1807,1810,1813,1816,1819],{"class":67,"line":92},[65,1805,1806],{"class":1541}," if",[65,1808,1809],{"class":1389}," (e.duration ",[65,1811,1812],{"class":1541},">",[65,1814,1815],{"class":1574}," 100",[65,1817,1818],{"class":1389},") { ",[65,1820,1821],{"class":1379},"// log queries over 100ms\n",[65,1823,1824,1827,1830,1832,1835,1837,1839,1842,1845,1847,1849,1852,1855],{"class":67,"line":98},[65,1825,1826],{"class":1389}," console.",[65,1828,1829],{"class":1534},"warn",[65,1831,1783],{"class":1389},[65,1833,1834],{"class":1410},"`Slow query (${",[65,1836,1792],{"class":1389},[65,1838,758],{"class":1410},[65,1840,1841],{"class":1389},"duration",[65,1843,1844],{"class":1410},"}ms): ${",[65,1846,1792],{"class":1389},[65,1848,758],{"class":1410},[65,1850,1851],{"class":1389},"query",[65,1853,1854],{"class":1410},"}`",[65,1856,1857],{"class":1389},")\n",[65,1859,1860],{"class":67,"line":104},[65,1861,1862],{"class":1389}," }\n",[65,1864,1865],{"class":67,"line":110},[65,1866,1772],{"class":1389},[15,1868,1869],{},"For production, send these events to your observability platform (Datadog, Grafana, etc.) rather than logging to console.",[15,1871,1872,1875],{},[124,1873,1874],{},"N+1 query detection."," The N+1 problem is one of the most common performance anti-patterns when using ORMs. It occurs when loading a list of N records triggers N additional queries to load related data — one for each record.",[15,1877,1878],{},"Example of N+1 with Prisma:",[56,1880,1882],{"className":1726,"code":1881,"language":1728,"meta":61,"style":61},"// This fires 1 query for users, then 1 query per user for their posts\nconst users = await prisma.user.findMany()\nfor (const user of users) {\n const posts = await prisma.post.findMany({ where: { authorId: user.id } })\n}\n\n// This fires 1 query with a JOIN — the correct approach\nconst users = await prisma.user.findMany({\n include: { posts: true }\n})\n",[33,1883,1884,1889,1910,1928,1948,1953,1957,1962,1978,1988],{"__ignoreMap":61},[65,1885,1886],{"class":67,"line":68},[65,1887,1888],{"class":1379},"// This fires 1 query for users, then 1 query per user for their posts\n",[65,1890,1891,1893,1896,1898,1901,1904,1907],{"class":67,"line":74},[65,1892,1735],{"class":1541},[65,1894,1895],{"class":1574}," users",[65,1897,1741],{"class":1541},[65,1899,1900],{"class":1541}," await",[65,1902,1903],{"class":1389}," prisma.user.",[65,1905,1906],{"class":1534},"findMany",[65,1908,1909],{"class":1389},"()\n",[65,1911,1912,1915,1917,1919,1922,1925],{"class":67,"line":80},[65,1913,1914],{"class":1541},"for",[65,1916,538],{"class":1389},[65,1918,1735],{"class":1541},[65,1920,1921],{"class":1574}," user",[65,1923,1924],{"class":1541}," of",[65,1926,1927],{"class":1389}," users) {\n",[65,1929,1930,1933,1936,1938,1940,1943,1945],{"class":67,"line":86},[65,1931,1932],{"class":1541}," const",[65,1934,1935],{"class":1574}," posts",[65,1937,1741],{"class":1541},[65,1939,1900],{"class":1541},[65,1941,1942],{"class":1389}," prisma.post.",[65,1944,1906],{"class":1534},[65,1946,1947],{"class":1389},"({ where: { authorId: user.id } })\n",[65,1949,1950],{"class":67,"line":92},[65,1951,1952],{"class":1389},"}\n",[65,1954,1955],{"class":67,"line":98},[65,1956,279],{"emptyLinePlaceholder":278},[65,1958,1959],{"class":67,"line":104},[65,1960,1961],{"class":1379},"// This fires 1 query with a JOIN — the correct approach\n",[65,1963,1964,1966,1968,1970,1972,1974,1976],{"class":67,"line":110},[65,1965,1735],{"class":1541},[65,1967,1895],{"class":1574},[65,1969,1741],{"class":1541},[65,1971,1900],{"class":1541},[65,1973,1903],{"class":1389},[65,1975,1906],{"class":1534},[65,1977,1750],{"class":1389},[65,1979,1980,1983,1986],{"class":67,"line":367},[65,1981,1982],{"class":1389}," include: { posts: ",[65,1984,1985],{"class":1574},"true",[65,1987,1862],{"class":1389},[65,1989,1990],{"class":67,"line":431},[65,1991,1772],{"class":1389},[15,1993,1994,1995,1998,1999,2002],{},"ORM-level tooling like ",[33,1996,1997],{},"prisma-query-inspector"," or ",[33,2000,2001],{},"knex-query-debug"," can detect N+1 patterns automatically. In tests, you can assert on query count to catch regressions.",[746,2004],{},[22,2006,2008],{"id":2007},"understanding-explain","Understanding EXPLAIN",[15,2010,2011,2013],{},[33,2012,53],{}," is the most important diagnostic tool for slow queries. It shows the query execution plan — how PostgreSQL decided to execute your query — along with actual execution statistics.",[56,2015,2017],{"className":58,"code":2016,"language":60,"meta":61,"style":61},"EXPLAIN ANALYZE\nSELECT u.*, p.*\nFROM users u\nJOIN posts p ON p.author_id = u.id\nWHERE u.organization_id = '123'\nAND p.published_at > NOW() - INTERVAL '30 days'\nORDER BY p.published_at DESC\nLIMIT 20;\n",[33,2018,2019,2023,2028,2032,2037,2042,2047,2052],{"__ignoreMap":61},[65,2020,2021],{"class":67,"line":68},[65,2022,71],{},[65,2024,2025],{"class":67,"line":74},[65,2026,2027],{},"SELECT u.*, p.*\n",[65,2029,2030],{"class":67,"line":80},[65,2031,83],{},[65,2033,2034],{"class":67,"line":86},[65,2035,2036],{},"JOIN posts p ON p.author_id = u.id\n",[65,2038,2039],{"class":67,"line":92},[65,2040,2041],{},"WHERE u.organization_id = '123'\n",[65,2043,2044],{"class":67,"line":98},[65,2045,2046],{},"AND p.published_at > NOW() - INTERVAL '30 days'\n",[65,2048,2049],{"class":67,"line":104},[65,2050,2051],{},"ORDER BY p.published_at DESC\n",[65,2053,2054],{"class":67,"line":110},[65,2055,113],{},[15,2057,2058],{},"Key things to look for in the output:",[15,2060,2061,2064],{},[124,2062,2063],{},"Sequential scan (Seq Scan):"," The database is reading every row in the table. If your table has millions of rows, this is slow. Fix: add an index on the filtered column.",[15,2066,2067,2070],{},[124,2068,2069],{},"Index scan:"," The database is using an index to find rows directly. This is what you want.",[15,2072,2073,2076],{},[124,2074,2075],{},"Nested loop join:"," For small result sets, this is fine. For large tables, this can be slow if the inner loop executes many times. Consider the join order and indexing of join keys.",[15,2078,2079,2082,2083,2086],{},[124,2080,2081],{},"High row estimates vs. Actual rows:"," If the planner estimates 10 rows but actually fetches 10,000, the statistics are stale. Run ",[33,2084,2085],{},"ANALYZE tablename"," to update statistics, or tune autovacuum to run more frequently.",[15,2088,2089,2092],{},[124,2090,2091],{},"Costs:"," The numbers in parentheses are planner cost estimates. High costs indicate expensive operations. Compare the estimated startup cost to the actual execution time to understand the planning accuracy.",[746,2094],{},[22,2096,2098],{"id":2097},"the-indexes-that-matter","The Indexes That Matter",[15,2100,2101],{},"An index is a data structure that allows the database to find rows quickly without scanning the entire table. Building the right indexes is the most impactful optimization for read-heavy queries.",[15,2103,2104],{},[124,2105,2106],{},"Index the columns in your WHERE clauses:",[56,2108,2110],{"className":58,"code":2109,"language":60,"meta":61,"style":61},"-- Slow without index\nWHERE organization_id = '123'\n\n-- Create a covering index\nCREATE INDEX idx_users_org_id ON users(organization_id);\n",[33,2111,2112,2117,2122,2126,2131],{"__ignoreMap":61},[65,2113,2114],{"class":67,"line":68},[65,2115,2116],{},"-- Slow without index\n",[65,2118,2119],{"class":67,"line":74},[65,2120,2121],{},"WHERE organization_id = '123'\n",[65,2123,2124],{"class":67,"line":80},[65,2125,279],{"emptyLinePlaceholder":278},[65,2127,2128],{"class":67,"line":86},[65,2129,2130],{},"-- Create a covering index\n",[65,2132,2133],{"class":67,"line":92},[65,2134,2135],{},"CREATE INDEX idx_users_org_id ON users(organization_id);\n",[15,2137,2138],{},[124,2139,2140],{},"Composite indexes for multi-column filters:",[56,2142,2144],{"className":58,"code":2143,"language":60,"meta":61,"style":61},"-- This query benefits from a composite index\nWHERE organization_id = '123' AND status = 'active'\n\n-- Column order matters: put the most selective column first\nCREATE INDEX idx_users_org_status ON users(organization_id, status);\n",[33,2145,2146,2151,2156,2160,2165],{"__ignoreMap":61},[65,2147,2148],{"class":67,"line":68},[65,2149,2150],{},"-- This query benefits from a composite index\n",[65,2152,2153],{"class":67,"line":74},[65,2154,2155],{},"WHERE organization_id = '123' AND status = 'active'\n",[65,2157,2158],{"class":67,"line":80},[65,2159,279],{"emptyLinePlaceholder":278},[65,2161,2162],{"class":67,"line":86},[65,2163,2164],{},"-- Column order matters: put the most selective column first\n",[65,2166,2167],{"class":67,"line":92},[65,2168,2169],{},"CREATE INDEX idx_users_org_status ON users(organization_id, status);\n",[15,2171,2172],{},[124,2173,2174],{},"Index for sort operations:",[56,2176,2178],{"className":58,"code":2177,"language":60,"meta":61,"style":61},"-- Without an index, sorting requires a full scan + sort\nORDER BY created_at DESC\n\n-- With the index, the database can scan in reverse order\nCREATE INDEX idx_posts_created_at ON posts(created_at DESC);\n",[33,2179,2180,2185,2190,2194,2199],{"__ignoreMap":61},[65,2181,2182],{"class":67,"line":68},[65,2183,2184],{},"-- Without an index, sorting requires a full scan + sort\n",[65,2186,2187],{"class":67,"line":74},[65,2188,2189],{},"ORDER BY created_at DESC\n",[65,2191,2192],{"class":67,"line":80},[65,2193,279],{"emptyLinePlaceholder":278},[65,2195,2196],{"class":67,"line":86},[65,2197,2198],{},"-- With the index, the database can scan in reverse order\n",[65,2200,2201],{"class":67,"line":92},[65,2202,2203],{},"CREATE INDEX idx_posts_created_at ON posts(created_at DESC);\n",[15,2205,2206],{},[124,2207,2208],{},"Partial indexes for common filter patterns:",[56,2210,2212],{"className":58,"code":2211,"language":60,"meta":61,"style":61},"-- If you frequently query for active subscriptions\nWHERE status = 'active'\n\n-- A partial index is smaller and faster than a full index\nCREATE INDEX idx_subscriptions_active ON subscriptions(user_id) WHERE status = 'active';\n",[33,2213,2214,2219,2223,2227,2232],{"__ignoreMap":61},[65,2215,2216],{"class":67,"line":68},[65,2217,2218],{},"-- If you frequently query for active subscriptions\n",[65,2220,2221],{"class":67,"line":74},[65,2222,405],{},[65,2224,2225],{"class":67,"line":80},[65,2226,279],{"emptyLinePlaceholder":278},[65,2228,2229],{"class":67,"line":86},[65,2230,2231],{},"-- A partial index is smaller and faster than a full index\n",[65,2233,2234],{"class":67,"line":92},[65,2235,2236],{},"CREATE INDEX idx_subscriptions_active ON subscriptions(user_id) WHERE status = 'active';\n",[15,2238,2239,2242,2243,2246],{},[124,2240,2241],{},"The index that doesn't help:"," An index on a column with very low cardinality (few distinct values) — like a boolean ",[33,2244,2245],{},"is_deleted"," column — rarely helps because the database often decides a sequential scan is cheaper than using the index for columns where the index doesn't meaningfully narrow the search.",[746,2248],{},[22,2250,2252],{"id":2251},"common-query-anti-patterns","Common Query Anti-Patterns",[15,2254,2255,2261],{},[124,2256,2257,2260],{},[33,2258,2259],{},"SELECT *"," when you need a few columns."," Fetching all columns transfers more data than necessary and prevents covering index optimizations. Select only the columns you actually use.",[15,2263,2264],{},[124,2265,2266],{},"Functions in WHERE clauses that prevent index use:",[56,2268,2270],{"className":58,"code":2269,"language":60,"meta":61,"style":61},"-- This can't use an index on created_at\nWHERE YEAR(created_at) = 2025\n\n-- This can\nWHERE created_at >= '2025-01-01' AND created_at \u003C '2026-01-01'\n",[33,2271,2272,2277,2282,2286,2291],{"__ignoreMap":61},[65,2273,2274],{"class":67,"line":68},[65,2275,2276],{},"-- This can't use an index on created_at\n",[65,2278,2279],{"class":67,"line":74},[65,2280,2281],{},"WHERE YEAR(created_at) = 2025\n",[65,2283,2284],{"class":67,"line":80},[65,2285,279],{"emptyLinePlaceholder":278},[65,2287,2288],{"class":67,"line":86},[65,2289,2290],{},"-- This can\n",[65,2292,2293],{"class":67,"line":92},[65,2294,2295],{},"WHERE created_at >= '2025-01-01' AND created_at \u003C '2026-01-01'\n",[15,2297,2298],{},[124,2299,2300],{},"LIKE with a leading wildcard:",[56,2302,2304],{"className":58,"code":2303,"language":60,"meta":61,"style":61},"-- Can't use a B-tree index\nWHERE email LIKE '%gmail.com'\n\n-- Can use an index\nWHERE email LIKE 'james%'\n",[33,2305,2306,2311,2316,2320,2325],{"__ignoreMap":61},[65,2307,2308],{"class":67,"line":68},[65,2309,2310],{},"-- Can't use a B-tree index\n",[65,2312,2313],{"class":67,"line":74},[65,2314,2315],{},"WHERE email LIKE '%gmail.com'\n",[65,2317,2318],{"class":67,"line":80},[65,2319,279],{"emptyLinePlaceholder":278},[65,2321,2322],{"class":67,"line":86},[65,2323,2324],{},"-- Can use an index\n",[65,2326,2327],{"class":67,"line":92},[65,2328,2329],{},"WHERE email LIKE 'james%'\n",[15,2331,2332,2333,2336,2337,2340],{},"For full-text search with leading wildcards, use PostgreSQL's full-text search capabilities (",[33,2334,2335],{},"tsvector","/",[33,2338,2339],{},"tsquery",") or an external search index (Elasticsearch, Meilisearch).",[15,2342,2343,2346,2347,2350],{},[124,2344,2345],{},"DISTINCT to cover up a bad join."," If you're adding ",[33,2348,2349],{},"DISTINCT"," because your query is returning duplicate rows, that's usually a symptom of an incorrect join that needs to be fixed rather than filtered.",[15,2352,2353,2356,2357,2360],{},[124,2354,2355],{},"Large OFFSET for pagination."," ",[33,2358,2359],{},"OFFSET 10000 LIMIT 20"," requires the database to scan and discard 10,000 rows before returning 20. Use cursor-based pagination (keyset pagination) instead:",[56,2362,2364],{"className":58,"code":2363,"language":60,"meta":61,"style":61},"-- Instead of OFFSET\nWHERE id > :lastSeenId\nORDER BY id\nLIMIT 20\n",[33,2365,2366,2371,2376,2381],{"__ignoreMap":61},[65,2367,2368],{"class":67,"line":68},[65,2369,2370],{},"-- Instead of OFFSET\n",[65,2372,2373],{"class":67,"line":74},[65,2374,2375],{},"WHERE id > :lastSeenId\n",[65,2377,2378],{"class":67,"line":80},[65,2379,2380],{},"ORDER BY id\n",[65,2382,2383],{"class":67,"line":86},[65,2384,2385],{},"LIMIT 20\n",[746,2387],{},[22,2389,2391],{"id":2390},"query-optimization-workflow","Query Optimization Workflow",[15,2393,2394],{},"When a query is identified as slow, I work through this sequence:",[2396,2397,2398,2404,2407,2410,2416,2419],"ol",{},[121,2399,2400,2401,2403],{},"Run ",[33,2402,53],{}," on the exact query with representative parameters.",[121,2405,2406],{},"Identify the most expensive operation (highest row estimate, sequential scans on large tables).",[121,2408,2409],{},"Check whether an index would help — look at the columns in the WHERE clause, JOIN conditions, and ORDER BY.",[121,2411,2412,2413,2415],{},"Add the index and re-run ",[33,2414,53],{}," to verify the plan changed.",[121,2417,2418],{},"Measure the actual improvement in production with query timing metrics.",[121,2420,2421],{},"Check whether denormalization would help for frequently-accessed aggregates (cache a computed column rather than aggregating on every request).",[746,2423],{},[15,2425,2426,2427,2430],{},"Database query performance is one of the most tractable performance problems in web development — the diagnostic tools are good, the fixes are often straightforward, and the improvements are measurable and permanent. If you're working on an application with slow API response times and suspect the database is the bottleneck, book a call at ",[752,2428,757],{"href":754,"rel":2429},[756]," and let's find the slow queries together.",[746,2432],{},[22,2434,764],{"id":763},[118,2436,2437,2441,2445,2449],{},[121,2438,2439],{},[752,2440,778],{"href":777},[121,2442,2443],{},[752,2444,7],{"href":818},[121,2446,2447],{},[752,2448,772],{"href":771},[121,2450,2451],{},[752,2452,790],{"href":789},[792,2454,2455],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":80,"depth":80,"links":2457},[2458,2459,2460,2461,2462,2463,2464],{"id":1662,"depth":74,"text":1663},{"id":1677,"depth":74,"text":1678},{"id":2007,"depth":74,"text":2008},{"id":2097,"depth":74,"text":2098},{"id":2251,"depth":74,"text":2252},{"id":2390,"depth":74,"text":2391},{"id":763,"depth":74,"text":764},"Slow database queries are the most common cause of sluggish API responses. Here's a systematic approach to finding them, understanding why they're slow, and fixing them.",[2467,2468],"database query performance","slow query optimization",{},{"title":784,"description":2465},"blog/database-query-performance",[822,824,823],"BG1vseKCtv22MxUWh_1IuFaiRGhkCUlvcMRQRApZcJg",{"id":2475,"title":2476,"author":2477,"body":2478,"category":808,"date":809,"description":3725,"extension":811,"featured":812,"image":813,"keywords":3726,"meta":3729,"navigation":278,"path":3730,"readTime":104,"seo":3731,"stem":3732,"tags":3733,"__hash__":3735},"blog/blog/database-transactions-guide.md","Database Transactions: ACID, Isolation Levels, and When It All Goes Wrong",{"name":9,"bio":10},{"type":12,"value":2479,"toc":3716},[2480,2483,2486,2490,2496,2502,2508,2514,2518,2521,2527,2530,2536,2579,2585,2625,2631,2656,2662,2665,2669,2672,2677,2688,2693,2704,2709,2720,2723,2743,2746,2805,2809,2815,2868,2871,3125,3131,3159,3171,3173,3326,3330,3333,3495,3498,3502,3505,3513,3516,3519,3678,3681,3683,3689,3691,3693,3713],[15,2481,2482],{},"Transactions are one of those topics that developers understand conceptually — group operations so they succeed or fail together — but misunderstand in practice. The subtleties of isolation levels, the difference between phantom reads and non-repeatable reads, and when you actually need serializable isolation all matter in production systems where concurrent users are modifying shared data.",[15,2484,2485],{},"This guide is about the practical side: the bugs that happen without proper transaction isolation and how to fix them.",[22,2487,2489],{"id":2488},"acid-what-each-property-actually-means","ACID: What Each Property Actually Means",[15,2491,2492,2495],{},[124,2493,2494],{},"Atomicity:"," The transaction succeeds completely or fails completely. If you transfer $100 from account A to account B and the debit succeeds but the credit fails, the debit is rolled back. The database never has a state where $100 was debited but not credited.",[15,2497,2498,2501],{},[124,2499,2500],{},"Consistency:"," A transaction brings the database from one valid state to another valid state. Constraints, triggers, and cascades are enforced. You cannot end a transaction with a violated foreign key.",[15,2503,2504,2507],{},[124,2505,2506],{},"Isolation:"," Concurrent transactions behave as if they run serially. The degree of this guarantee is controlled by the isolation level — this is where most of the nuance lives.",[15,2509,2510,2513],{},[124,2511,2512],{},"Durability:"," Once a transaction is committed, it persists even through crashes. PostgreSQL uses write-ahead logging (WAL) to ensure committed data survives a crash.",[22,2515,2517],{"id":2516},"the-concurrency-anomalies","The Concurrency Anomalies",[15,2519,2520],{},"Without sufficient isolation, concurrent transactions can produce incorrect results. Understanding these anomalies is the key to choosing the right isolation level.",[15,2522,2523,2526],{},[124,2524,2525],{},"Dirty Read:"," Transaction A reads data that Transaction B has written but not yet committed. If Transaction B rolls back, Transaction A was working with data that never existed.",[15,2528,2529],{},"PostgreSQL's Read Committed isolation (the default) prevents dirty reads. Every read sees only committed data.",[15,2531,2532,2535],{},[124,2533,2534],{},"Non-Repeatable Read:"," Transaction A reads a row, Transaction B updates and commits it, Transaction A reads the same row again and gets a different value.",[56,2537,2539],{"className":58,"code":2538,"language":60,"meta":61,"style":61},"-- Transaction A\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1; -- returns 1000\n\n-- Transaction B commits an update: balance now = 900\n\nSELECT balance FROM accounts WHERE id = 1; -- returns 900 (different!)\nCOMMIT;\n",[33,2540,2541,2546,2551,2556,2560,2565,2569,2574],{"__ignoreMap":61},[65,2542,2543],{"class":67,"line":68},[65,2544,2545],{},"-- Transaction A\n",[65,2547,2548],{"class":67,"line":74},[65,2549,2550],{},"BEGIN;\n",[65,2552,2553],{"class":67,"line":80},[65,2554,2555],{},"SELECT balance FROM accounts WHERE id = 1; -- returns 1000\n",[65,2557,2558],{"class":67,"line":86},[65,2559,279],{"emptyLinePlaceholder":278},[65,2561,2562],{"class":67,"line":92},[65,2563,2564],{},"-- Transaction B commits an update: balance now = 900\n",[65,2566,2567],{"class":67,"line":98},[65,2568,279],{"emptyLinePlaceholder":278},[65,2570,2571],{"class":67,"line":104},[65,2572,2573],{},"SELECT balance FROM accounts WHERE id = 1; -- returns 900 (different!)\n",[65,2575,2576],{"class":67,"line":110},[65,2577,2578],{},"COMMIT;\n",[15,2580,2581,2584],{},[124,2582,2583],{},"Phantom Read:"," Transaction A reads a set of rows matching a condition, Transaction B inserts (or deletes) rows matching the same condition, Transaction A re-reads and gets different rows.",[56,2586,2588],{"className":58,"code":2587,"language":60,"meta":61,"style":61},"-- Transaction A\nBEGIN;\nSELECT COUNT(*) FROM orders WHERE status = 'pending'; -- returns 5\n\n-- Transaction B inserts a new pending order and commits\n\nSELECT COUNT(*) FROM orders WHERE status = 'pending'; -- returns 6 (phantom!)\nCOMMIT;\n",[33,2589,2590,2594,2598,2603,2607,2612,2616,2621],{"__ignoreMap":61},[65,2591,2592],{"class":67,"line":68},[65,2593,2545],{},[65,2595,2596],{"class":67,"line":74},[65,2597,2550],{},[65,2599,2600],{"class":67,"line":80},[65,2601,2602],{},"SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- returns 5\n",[65,2604,2605],{"class":67,"line":86},[65,2606,279],{"emptyLinePlaceholder":278},[65,2608,2609],{"class":67,"line":92},[65,2610,2611],{},"-- Transaction B inserts a new pending order and commits\n",[65,2613,2614],{"class":67,"line":98},[65,2615,279],{"emptyLinePlaceholder":278},[65,2617,2618],{"class":67,"line":104},[65,2619,2620],{},"SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- returns 6 (phantom!)\n",[65,2622,2623],{"class":67,"line":110},[65,2624,2578],{},[15,2626,2627,2630],{},[124,2628,2629],{},"Lost Update:"," Two transactions read the same value, both modify it, and one modification overwrites the other.",[56,2632,2634],{"className":58,"code":2633,"language":60,"meta":61,"style":61},"-- Transaction A reads balance = 1000, plans to add $100\n-- Transaction B reads balance = 1000, plans to add $200\n-- Transaction A writes 1100 and commits\n-- Transaction B writes 1200 and commits ← A's update is lost\n",[33,2635,2636,2641,2646,2651],{"__ignoreMap":61},[65,2637,2638],{"class":67,"line":68},[65,2639,2640],{},"-- Transaction A reads balance = 1000, plans to add $100\n",[65,2642,2643],{"class":67,"line":74},[65,2644,2645],{},"-- Transaction B reads balance = 1000, plans to add $200\n",[65,2647,2648],{"class":67,"line":80},[65,2649,2650],{},"-- Transaction A writes 1100 and commits\n",[65,2652,2653],{"class":67,"line":86},[65,2654,2655],{},"-- Transaction B writes 1200 and commits ← A's update is lost\n",[15,2657,2658,2661],{},[124,2659,2660],{},"Write Skew:"," Two transactions read overlapping data, make decisions based on what they read, and write different data — but the combination of their writes violates a constraint.",[15,2663,2664],{},"Classic example: two doctors are on call, at least one must always be on call. Both see the other is on call, both decide they can take the day off, both update simultaneously, and now zero doctors are on call.",[22,2666,2668],{"id":2667},"isolation-levels-in-postgresql","Isolation Levels in PostgreSQL",[15,2670,2671],{},"PostgreSQL implements four isolation levels (though it maps Read Uncommitted to Read Committed):",[15,2673,2674],{},[124,2675,2676],{},"Read Committed (default):",[118,2678,2679,2682,2685],{},[121,2680,2681],{},"Prevents: Dirty reads",[121,2683,2684],{},"Allows: Non-repeatable reads, phantom reads, write skew",[121,2686,2687],{},"Use for: Most web application read operations",[15,2689,2690],{},[124,2691,2692],{},"Repeatable Read:",[118,2694,2695,2698,2701],{},[121,2696,2697],{},"Prevents: Dirty reads, non-repeatable reads",[121,2699,2700],{},"Allows: Phantom reads (PostgreSQL actually prevents these too)",[121,2702,2703],{},"Use for: Reports and calculations that need a consistent snapshot",[15,2705,2706],{},[124,2707,2708],{},"Serializable:",[118,2710,2711,2714,2717],{},[121,2712,2713],{},"Prevents: All anomalies including write skew",[121,2715,2716],{},"More expensive: The database detects and aborts transactions that would produce non-serializable results",[121,2718,2719],{},"Use for: Financial operations, inventory management, anything where correctness is absolute",[15,2721,2722],{},"Set isolation level in PostgreSQL:",[56,2724,2726],{"className":58,"code":2725,"language":60,"meta":61,"style":61},"BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n-- or\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n",[33,2727,2728,2733,2738],{"__ignoreMap":61},[65,2729,2730],{"class":67,"line":68},[65,2731,2732],{},"BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n",[65,2734,2735],{"class":67,"line":74},[65,2736,2737],{},"-- or\n",[65,2739,2740],{"class":67,"line":80},[65,2741,2742],{},"BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n",[15,2744,2745],{},"In Prisma:",[56,2747,2749],{"className":1726,"code":2748,"language":1728,"meta":61,"style":61},"await prisma.$transaction(\n async (tx) => {\n // Operations here\n },\n { isolationLevel: 'Serializable' }\n)\n",[33,2750,2751,2765,2781,2786,2791,2801],{"__ignoreMap":61},[65,2752,2753,2756,2759,2762],{"class":67,"line":68},[65,2754,2755],{"class":1541},"await",[65,2757,2758],{"class":1389}," prisma.",[65,2760,2761],{"class":1534},"$transaction",[65,2763,2764],{"class":1389},"(\n",[65,2766,2767,2770,2772,2775,2777,2779],{"class":67,"line":74},[65,2768,2769],{"class":1541}," async",[65,2771,538],{"class":1389},[65,2773,2774],{"class":1791},"tx",[65,2776,1795],{"class":1389},[65,2778,1798],{"class":1541},[65,2780,1801],{"class":1389},[65,2782,2783],{"class":67,"line":80},[65,2784,2785],{"class":1379}," // Operations here\n",[65,2787,2788],{"class":67,"line":86},[65,2789,2790],{"class":1389}," },\n",[65,2792,2793,2796,2799],{"class":67,"line":92},[65,2794,2795],{"class":1389}," { isolationLevel: ",[65,2797,2798],{"class":1410},"'Serializable'",[65,2800,1862],{"class":1389},[65,2802,2803],{"class":67,"line":98},[65,2804,1857],{"class":1389},[22,2806,2808],{"id":2807},"fixing-the-lost-update-problem","Fixing the Lost Update Problem",[15,2810,2811,2814],{},[124,2812,2813],{},"Optimistic locking"," — add a version number and check it on update:",[56,2816,2818],{"className":58,"code":2817,"language":60,"meta":61,"style":61},"-- Add a version column\nALTER TABLE accounts ADD COLUMN version INTEGER NOT NULL DEFAULT 0;\n\n-- Update only if version matches what we read\nUPDATE accounts\nSET balance = balance + 100, version = version + 1\nWHERE id = $1 AND version = $readVersion;\n\n-- Check affected rows\n-- If 0 rows affected, someone else updated first — retry\n",[33,2819,2820,2825,2830,2834,2839,2844,2849,2854,2858,2863],{"__ignoreMap":61},[65,2821,2822],{"class":67,"line":68},[65,2823,2824],{},"-- Add a version column\n",[65,2826,2827],{"class":67,"line":74},[65,2828,2829],{},"ALTER TABLE accounts ADD COLUMN version INTEGER NOT NULL DEFAULT 0;\n",[65,2831,2832],{"class":67,"line":80},[65,2833,279],{"emptyLinePlaceholder":278},[65,2835,2836],{"class":67,"line":86},[65,2837,2838],{},"-- Update only if version matches what we read\n",[65,2840,2841],{"class":67,"line":92},[65,2842,2843],{},"UPDATE accounts\n",[65,2845,2846],{"class":67,"line":98},[65,2847,2848],{},"SET balance = balance + 100, version = version + 1\n",[65,2850,2851],{"class":67,"line":104},[65,2852,2853],{},"WHERE id = $1 AND version = $readVersion;\n",[65,2855,2856],{"class":67,"line":110},[65,2857,279],{"emptyLinePlaceholder":278},[65,2859,2860],{"class":67,"line":367},[65,2861,2862],{},"-- Check affected rows\n",[65,2864,2865],{"class":67,"line":431},[65,2866,2867],{},"-- If 0 rows affected, someone else updated first — retry\n",[15,2869,2870],{},"In application code:",[56,2872,2874],{"className":1726,"code":2873,"language":1728,"meta":61,"style":61},"async function updateBalance(accountId: string, amount: number, maxRetries = 3) {\n for (let attempt = 0; attempt \u003C maxRetries; attempt++) {\n const account = await prisma.account.findUniqueOrThrow({\n where: { id: accountId },\n })\n\n const updated = await prisma.account.updateMany({\n where: {\n id: accountId,\n version: account.version, // Optimistic lock check\n },\n data: {\n balance: account.balance + amount,\n version: { increment: 1 },\n },\n })\n\n if (updated.count === 1) return // Success\n // Otherwise, retry\n }\n\n throw new Error('Update failed after retries due to concurrent modification')\n}\n",[33,2875,2876,2922,2955,2974,2979,2984,2988,3006,3011,3016,3024,3028,3033,3044,3054,3058,3062,3066,3087,3092,3097,3102,3120],{"__ignoreMap":61},[65,2877,2878,2881,2884,2887,2889,2892,2895,2898,2901,2904,2906,2909,2911,2914,2916,2919],{"class":67,"line":68},[65,2879,2880],{"class":1541},"async",[65,2882,2883],{"class":1541}," function",[65,2885,2886],{"class":1534}," updateBalance",[65,2888,1783],{"class":1389},[65,2890,2891],{"class":1791},"accountId",[65,2893,2894],{"class":1541},":",[65,2896,2897],{"class":1574}," string",[65,2899,2900],{"class":1389},", ",[65,2902,2903],{"class":1791},"amount",[65,2905,2894],{"class":1541},[65,2907,2908],{"class":1574}," number",[65,2910,2900],{"class":1389},[65,2912,2913],{"class":1791},"maxRetries",[65,2915,1741],{"class":1541},[65,2917,2918],{"class":1574}," 3",[65,2920,2921],{"class":1389},") {\n",[65,2923,2924,2927,2929,2932,2935,2938,2941,2944,2947,2950,2953],{"class":67,"line":74},[65,2925,2926],{"class":1541}," for",[65,2928,538],{"class":1389},[65,2930,2931],{"class":1541},"let",[65,2933,2934],{"class":1389}," attempt ",[65,2936,2937],{"class":1541},"=",[65,2939,2940],{"class":1574}," 0",[65,2942,2943],{"class":1389},"; attempt ",[65,2945,2946],{"class":1541},"\u003C",[65,2948,2949],{"class":1389}," maxRetries; attempt",[65,2951,2952],{"class":1541},"++",[65,2954,2921],{"class":1389},[65,2956,2957,2959,2962,2964,2966,2969,2972],{"class":67,"line":80},[65,2958,1932],{"class":1541},[65,2960,2961],{"class":1574}," account",[65,2963,1741],{"class":1541},[65,2965,1900],{"class":1541},[65,2967,2968],{"class":1389}," prisma.account.",[65,2970,2971],{"class":1534},"findUniqueOrThrow",[65,2973,1750],{"class":1389},[65,2975,2976],{"class":67,"line":86},[65,2977,2978],{"class":1389}," where: { id: accountId },\n",[65,2980,2981],{"class":67,"line":92},[65,2982,2983],{"class":1389}," })\n",[65,2985,2986],{"class":67,"line":98},[65,2987,279],{"emptyLinePlaceholder":278},[65,2989,2990,2992,2995,2997,2999,3001,3004],{"class":67,"line":104},[65,2991,1932],{"class":1541},[65,2993,2994],{"class":1574}," updated",[65,2996,1741],{"class":1541},[65,2998,1900],{"class":1541},[65,3000,2968],{"class":1389},[65,3002,3003],{"class":1534},"updateMany",[65,3005,1750],{"class":1389},[65,3007,3008],{"class":67,"line":110},[65,3009,3010],{"class":1389}," where: {\n",[65,3012,3013],{"class":67,"line":367},[65,3014,3015],{"class":1389}," id: accountId,\n",[65,3017,3018,3021],{"class":67,"line":431},[65,3019,3020],{"class":1389}," version: account.version, ",[65,3022,3023],{"class":1379},"// Optimistic lock check\n",[65,3025,3026],{"class":67,"line":713},[65,3027,2790],{"class":1389},[65,3029,3030],{"class":67,"line":1043},[65,3031,3032],{"class":1389}," data: {\n",[65,3034,3035,3038,3041],{"class":67,"line":1049},[65,3036,3037],{"class":1389}," balance: account.balance ",[65,3039,3040],{"class":1541},"+",[65,3042,3043],{"class":1389}," amount,\n",[65,3045,3046,3049,3052],{"class":67,"line":1190},[65,3047,3048],{"class":1389}," version: { increment: ",[65,3050,3051],{"class":1574},"1",[65,3053,2790],{"class":1389},[65,3055,3056],{"class":67,"line":1196},[65,3057,2790],{"class":1389},[65,3059,3060],{"class":67,"line":1201},[65,3061,2983],{"class":1389},[65,3063,3064],{"class":67,"line":1207},[65,3065,279],{"emptyLinePlaceholder":278},[65,3067,3068,3070,3073,3076,3079,3081,3084],{"class":67,"line":1213},[65,3069,1806],{"class":1541},[65,3071,3072],{"class":1389}," (updated.count ",[65,3074,3075],{"class":1541},"===",[65,3077,3078],{"class":1574}," 1",[65,3080,1795],{"class":1389},[65,3082,3083],{"class":1541},"return",[65,3085,3086],{"class":1379}," // Success\n",[65,3088,3089],{"class":67,"line":1219},[65,3090,3091],{"class":1379}," // Otherwise, retry\n",[65,3093,3095],{"class":67,"line":3094},20,[65,3096,1862],{"class":1389},[65,3098,3100],{"class":67,"line":3099},21,[65,3101,279],{"emptyLinePlaceholder":278},[65,3103,3105,3108,3110,3113,3115,3118],{"class":67,"line":3104},22,[65,3106,3107],{"class":1541}," throw",[65,3109,1744],{"class":1541},[65,3111,3112],{"class":1534}," Error",[65,3114,1783],{"class":1389},[65,3116,3117],{"class":1410},"'Update failed after retries due to concurrent modification'",[65,3119,1857],{"class":1389},[65,3121,3123],{"class":67,"line":3122},23,[65,3124,1952],{"class":1389},[15,3126,3127,3130],{},[124,3128,3129],{},"Pessimistic locking"," — lock the row when reading it:",[56,3132,3134],{"className":58,"code":3133,"language":60,"meta":61,"style":61},"BEGIN;\nSELECT * FROM accounts WHERE id = 1 FOR UPDATE;\n-- Other transactions trying to modify this row will wait\nUPDATE accounts SET balance = balance + 100 WHERE id = 1;\nCOMMIT;\n",[33,3135,3136,3140,3145,3150,3155],{"__ignoreMap":61},[65,3137,3138],{"class":67,"line":68},[65,3139,2550],{},[65,3141,3142],{"class":67,"line":74},[65,3143,3144],{},"SELECT * FROM accounts WHERE id = 1 FOR UPDATE;\n",[65,3146,3147],{"class":67,"line":80},[65,3148,3149],{},"-- Other transactions trying to modify this row will wait\n",[65,3151,3152],{"class":67,"line":86},[65,3153,3154],{},"UPDATE accounts SET balance = balance + 100 WHERE id = 1;\n",[65,3156,3157],{"class":67,"line":92},[65,3158,2578],{},[15,3160,3161,3164,3165,2900,3167,3170],{},[33,3162,3163],{},"FOR UPDATE"," acquires a row-level lock. Other transactions that try to lock the same row (",[33,3166,3163],{},[33,3168,3169],{},"FOR SHARE",") will wait until this transaction commits or rolls back.",[15,3172,2745],{},[56,3174,3176],{"className":1726,"code":3175,"language":1728,"meta":61,"style":61},"await prisma.$transaction(async (tx) => {\n const account = await tx.$queryRaw\u003CAccount[]>`\n SELECT * FROM accounts WHERE id = ${accountId} FOR UPDATE\n `\n\n if (account[0].balance \u003C amount) {\n throw new Error('Insufficient funds')\n }\n\n await tx.account.update({\n where: { id: accountId },\n data: { balance: account[0].balance - amount },\n })\n})\n",[33,3177,3178,3200,3227,3237,3242,3246,3264,3279,3283,3287,3299,3303,3318,3322],{"__ignoreMap":61},[65,3179,3180,3182,3184,3186,3188,3190,3192,3194,3196,3198],{"class":67,"line":68},[65,3181,2755],{"class":1541},[65,3183,2758],{"class":1389},[65,3185,2761],{"class":1534},[65,3187,1783],{"class":1389},[65,3189,2880],{"class":1541},[65,3191,538],{"class":1389},[65,3193,2774],{"class":1791},[65,3195,1795],{"class":1389},[65,3197,1798],{"class":1541},[65,3199,1801],{"class":1389},[65,3201,3202,3204,3206,3208,3210,3213,3216,3218,3221,3224],{"class":67,"line":74},[65,3203,1932],{"class":1541},[65,3205,2961],{"class":1574},[65,3207,1741],{"class":1541},[65,3209,1900],{"class":1541},[65,3211,3212],{"class":1389}," tx.",[65,3214,3215],{"class":1534},"$queryRaw",[65,3217,2946],{"class":1389},[65,3219,3220],{"class":1534},"Account",[65,3222,3223],{"class":1389},"[]>",[65,3225,3226],{"class":1410},"`\n",[65,3228,3229,3232,3234],{"class":67,"line":80},[65,3230,3231],{"class":1410}," SELECT * FROM accounts WHERE id = ${",[65,3233,2891],{"class":1389},[65,3235,3236],{"class":1410},"} FOR UPDATE\n",[65,3238,3239],{"class":67,"line":86},[65,3240,3241],{"class":1410}," `\n",[65,3243,3244],{"class":67,"line":92},[65,3245,279],{"emptyLinePlaceholder":278},[65,3247,3248,3250,3253,3256,3259,3261],{"class":67,"line":98},[65,3249,1806],{"class":1541},[65,3251,3252],{"class":1389}," (account[",[65,3254,3255],{"class":1574},"0",[65,3257,3258],{"class":1389},"].balance ",[65,3260,2946],{"class":1541},[65,3262,3263],{"class":1389}," amount) {\n",[65,3265,3266,3268,3270,3272,3274,3277],{"class":67,"line":104},[65,3267,3107],{"class":1541},[65,3269,1744],{"class":1541},[65,3271,3112],{"class":1534},[65,3273,1783],{"class":1389},[65,3275,3276],{"class":1410},"'Insufficient funds'",[65,3278,1857],{"class":1389},[65,3280,3281],{"class":67,"line":110},[65,3282,1862],{"class":1389},[65,3284,3285],{"class":67,"line":367},[65,3286,279],{"emptyLinePlaceholder":278},[65,3288,3289,3291,3294,3297],{"class":67,"line":431},[65,3290,1900],{"class":1541},[65,3292,3293],{"class":1389}," tx.account.",[65,3295,3296],{"class":1534},"update",[65,3298,1750],{"class":1389},[65,3300,3301],{"class":67,"line":713},[65,3302,2978],{"class":1389},[65,3304,3305,3308,3310,3312,3315],{"class":67,"line":1043},[65,3306,3307],{"class":1389}," data: { balance: account[",[65,3309,3255],{"class":1574},[65,3311,3258],{"class":1389},[65,3313,3314],{"class":1541},"-",[65,3316,3317],{"class":1389}," amount },\n",[65,3319,3320],{"class":67,"line":1049},[65,3321,2983],{"class":1389},[65,3323,3324],{"class":67,"line":1190},[65,3325,1772],{"class":1389},[22,3327,3329],{"id":3328},"fixing-write-skew-with-serializable-isolation","Fixing Write Skew With Serializable Isolation",[15,3331,3332],{},"Write skew (the on-call doctors problem) requires serializable isolation — nothing else prevents it:",[56,3334,3336],{"className":1726,"code":3335,"language":1728,"meta":61,"style":61},"await prisma.$transaction(\n async (tx) => {\n // Read the current state\n const onCallDoctors = await tx.doctor.count({\n where: { onCall: true },\n })\n\n // Make a decision based on it\n if (onCallDoctors \u003C= 1) {\n throw new Error('Cannot go off-call: minimum 1 doctor required')\n }\n\n // Write based on the decision\n await tx.doctor.update({\n where: { id: doctorId },\n data: { onCall: false },\n })\n },\n { isolationLevel: 'Serializable' }\n)\n",[33,3337,3338,3348,3362,3367,3386,3395,3399,3403,3408,3422,3437,3441,3445,3450,3460,3465,3475,3479,3483,3491],{"__ignoreMap":61},[65,3339,3340,3342,3344,3346],{"class":67,"line":68},[65,3341,2755],{"class":1541},[65,3343,2758],{"class":1389},[65,3345,2761],{"class":1534},[65,3347,2764],{"class":1389},[65,3349,3350,3352,3354,3356,3358,3360],{"class":67,"line":74},[65,3351,2769],{"class":1541},[65,3353,538],{"class":1389},[65,3355,2774],{"class":1791},[65,3357,1795],{"class":1389},[65,3359,1798],{"class":1541},[65,3361,1801],{"class":1389},[65,3363,3364],{"class":67,"line":80},[65,3365,3366],{"class":1379}," // Read the current state\n",[65,3368,3369,3371,3374,3376,3378,3381,3384],{"class":67,"line":86},[65,3370,1932],{"class":1541},[65,3372,3373],{"class":1574}," onCallDoctors",[65,3375,1741],{"class":1541},[65,3377,1900],{"class":1541},[65,3379,3380],{"class":1389}," tx.doctor.",[65,3382,3383],{"class":1534},"count",[65,3385,1750],{"class":1389},[65,3387,3388,3391,3393],{"class":67,"line":92},[65,3389,3390],{"class":1389}," where: { onCall: ",[65,3392,1985],{"class":1574},[65,3394,2790],{"class":1389},[65,3396,3397],{"class":67,"line":98},[65,3398,2983],{"class":1389},[65,3400,3401],{"class":67,"line":104},[65,3402,279],{"emptyLinePlaceholder":278},[65,3404,3405],{"class":67,"line":110},[65,3406,3407],{"class":1379}," // Make a decision based on it\n",[65,3409,3410,3412,3415,3418,3420],{"class":67,"line":367},[65,3411,1806],{"class":1541},[65,3413,3414],{"class":1389}," (onCallDoctors ",[65,3416,3417],{"class":1541},"\u003C=",[65,3419,3078],{"class":1574},[65,3421,2921],{"class":1389},[65,3423,3424,3426,3428,3430,3432,3435],{"class":67,"line":431},[65,3425,3107],{"class":1541},[65,3427,1744],{"class":1541},[65,3429,3112],{"class":1534},[65,3431,1783],{"class":1389},[65,3433,3434],{"class":1410},"'Cannot go off-call: minimum 1 doctor required'",[65,3436,1857],{"class":1389},[65,3438,3439],{"class":67,"line":713},[65,3440,1862],{"class":1389},[65,3442,3443],{"class":67,"line":1043},[65,3444,279],{"emptyLinePlaceholder":278},[65,3446,3447],{"class":67,"line":1049},[65,3448,3449],{"class":1379}," // Write based on the decision\n",[65,3451,3452,3454,3456,3458],{"class":67,"line":1190},[65,3453,1900],{"class":1541},[65,3455,3380],{"class":1389},[65,3457,3296],{"class":1534},[65,3459,1750],{"class":1389},[65,3461,3462],{"class":67,"line":1196},[65,3463,3464],{"class":1389}," where: { id: doctorId },\n",[65,3466,3467,3470,3473],{"class":67,"line":1201},[65,3468,3469],{"class":1389}," data: { onCall: ",[65,3471,3472],{"class":1574},"false",[65,3474,2790],{"class":1389},[65,3476,3477],{"class":67,"line":1207},[65,3478,2983],{"class":1389},[65,3480,3481],{"class":67,"line":1213},[65,3482,2790],{"class":1389},[65,3484,3485,3487,3489],{"class":67,"line":1219},[65,3486,2795],{"class":1389},[65,3488,2798],{"class":1410},[65,3490,1862],{"class":1389},[65,3492,3493],{"class":67,"line":3094},[65,3494,1857],{"class":1389},[15,3496,3497],{},"With serializable isolation, if two transactions execute this simultaneously and both would produce an invalid state, PostgreSQL aborts one of them with a serialization failure error. Your application catches this and retries.",[22,3499,3501],{"id":3500},"deadlocks","Deadlocks",[15,3503,3504],{},"Deadlocks happen when two transactions each hold a lock that the other needs:",[118,3506,3507,3510],{},[121,3508,3509],{},"Transaction A locks row X, waits for row Y",[121,3511,3512],{},"Transaction B locks row Y, waits for row X",[15,3514,3515],{},"PostgreSQL detects deadlocks automatically and aborts one transaction (with error code 40P01). The application should retry on this error.",[15,3517,3518],{},"Prevent deadlocks by always acquiring locks in the same order:",[56,3520,3522],{"className":1726,"code":3521,"language":1728,"meta":61,"style":61},"// BAD: transactions might acquire locks in different orders\n// Transaction A: lock user 1, then lock account 1\n// Transaction B: lock account 1, then lock user 1\n\n// GOOD: always lock in a consistent order\nasync function transfer(fromId: string, toId: string, amount: number) {\n // Always lock the smaller ID first\n const [first, second] = [fromId, toId].sort()\n\n await prisma.$transaction(async (tx) => {\n await tx.$queryRaw`SELECT id FROM accounts WHERE id IN (${first}, ${second}) FOR UPDATE`\n // Now proceed with the transfer\n })\n}\n",[33,3523,3524,3529,3534,3539,3543,3548,3585,3590,3618,3622,3644,3665,3670,3674],{"__ignoreMap":61},[65,3525,3526],{"class":67,"line":68},[65,3527,3528],{"class":1379},"// BAD: transactions might acquire locks in different orders\n",[65,3530,3531],{"class":67,"line":74},[65,3532,3533],{"class":1379},"// Transaction A: lock user 1, then lock account 1\n",[65,3535,3536],{"class":67,"line":80},[65,3537,3538],{"class":1379},"// Transaction B: lock account 1, then lock user 1\n",[65,3540,3541],{"class":67,"line":86},[65,3542,279],{"emptyLinePlaceholder":278},[65,3544,3545],{"class":67,"line":92},[65,3546,3547],{"class":1379},"// GOOD: always lock in a consistent order\n",[65,3549,3550,3552,3554,3557,3559,3562,3564,3566,3568,3571,3573,3575,3577,3579,3581,3583],{"class":67,"line":98},[65,3551,2880],{"class":1541},[65,3553,2883],{"class":1541},[65,3555,3556],{"class":1534}," transfer",[65,3558,1783],{"class":1389},[65,3560,3561],{"class":1791},"fromId",[65,3563,2894],{"class":1541},[65,3565,2897],{"class":1574},[65,3567,2900],{"class":1389},[65,3569,3570],{"class":1791},"toId",[65,3572,2894],{"class":1541},[65,3574,2897],{"class":1574},[65,3576,2900],{"class":1389},[65,3578,2903],{"class":1791},[65,3580,2894],{"class":1541},[65,3582,2908],{"class":1574},[65,3584,2921],{"class":1389},[65,3586,3587],{"class":67,"line":104},[65,3588,3589],{"class":1379}," // Always lock the smaller ID first\n",[65,3591,3592,3594,3597,3600,3602,3605,3608,3610,3613,3616],{"class":67,"line":110},[65,3593,1932],{"class":1541},[65,3595,3596],{"class":1389}," [",[65,3598,3599],{"class":1574},"first",[65,3601,2900],{"class":1389},[65,3603,3604],{"class":1574},"second",[65,3606,3607],{"class":1389},"] ",[65,3609,2937],{"class":1541},[65,3611,3612],{"class":1389}," [fromId, toId].",[65,3614,3615],{"class":1534},"sort",[65,3617,1909],{"class":1389},[65,3619,3620],{"class":67,"line":367},[65,3621,279],{"emptyLinePlaceholder":278},[65,3623,3624,3626,3628,3630,3632,3634,3636,3638,3640,3642],{"class":67,"line":431},[65,3625,1900],{"class":1541},[65,3627,2758],{"class":1389},[65,3629,2761],{"class":1534},[65,3631,1783],{"class":1389},[65,3633,2880],{"class":1541},[65,3635,538],{"class":1389},[65,3637,2774],{"class":1791},[65,3639,1795],{"class":1389},[65,3641,1798],{"class":1541},[65,3643,1801],{"class":1389},[65,3645,3646,3648,3650,3652,3655,3657,3660,3662],{"class":67,"line":713},[65,3647,1900],{"class":1541},[65,3649,3212],{"class":1389},[65,3651,3215],{"class":1534},[65,3653,3654],{"class":1410},"`SELECT id FROM accounts WHERE id IN (${",[65,3656,3599],{"class":1389},[65,3658,3659],{"class":1410},"}, ${",[65,3661,3604],{"class":1389},[65,3663,3664],{"class":1410},"}) FOR UPDATE`\n",[65,3666,3667],{"class":67,"line":1043},[65,3668,3669],{"class":1379}," // Now proceed with the transfer\n",[65,3671,3672],{"class":67,"line":1049},[65,3673,2983],{"class":1389},[65,3675,3676],{"class":67,"line":1190},[65,3677,1952],{"class":1389},[15,3679,3680],{},"Transactions are a deep topic, but for most web applications, the default Read Committed isolation with optimistic locking for concurrent modifications covers the majority of cases. Reach for Serializable isolation deliberately, understand its retry requirements, and benchmark the performance impact for your specific workload.",[746,3682],{},[15,3684,3685,3686,758],{},"Dealing with data consistency issues in a concurrent application, or designing a transaction strategy for a financial or inventory system? This is exactly the kind of problem I work through with clients. Book a call: ",[752,3687,757],{"href":754,"rel":3688},[756],[746,3690],{},[22,3692,764],{"id":763},[118,3694,3695,3701,3705,3709],{},[121,3696,3697],{},[752,3698,3700],{"href":3699},"/blog/postgresql-row-level-security","PostgreSQL Row-Level Security: Data Isolation at the Database Layer",[121,3702,3703],{},[752,3704,772],{"href":771},[121,3706,3707],{},[752,3708,778],{"href":777},[121,3710,3711],{},[752,3712,7],{"href":818},[792,3714,3715],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":61,"searchDepth":80,"depth":80,"links":3717},[3718,3719,3720,3721,3722,3723,3724],{"id":2488,"depth":74,"text":2489},{"id":2516,"depth":74,"text":2517},{"id":2667,"depth":74,"text":2668},{"id":2807,"depth":74,"text":2808},{"id":3328,"depth":74,"text":3329},{"id":3500,"depth":74,"text":3501},{"id":763,"depth":74,"text":764},"A practical guide to database transactions — ACID properties, isolation levels, common concurrency bugs (dirty reads, phantoms, lost updates), and how to pick the right isolation level.",[3727,3728],"database transactions","ACID",{},"/blog/database-transactions-guide",{"title":2476,"description":3725},"blog/database-transactions-guide",[822,823,3734],"Transactions","VrIIjWI4plyLnF1GtAtyWucx4wvJZMR8xZORjetDXPk",{"id":3737,"title":3738,"author":3739,"body":3740,"category":4527,"date":809,"description":4528,"extension":811,"featured":812,"image":813,"keywords":4529,"meta":4531,"navigation":278,"path":4532,"readTime":104,"seo":4533,"stem":4534,"tags":4535,"__hash__":4538},"blog/blog/dependency-vulnerability-management.md","Dependency Vulnerability Management: Keeping Third-Party Code Safe",{"name":9,"bio":10},{"type":12,"value":3741,"toc":4516},[3742,3746,3753,3756,3760,3763,3776,3779,3787,3790,3813,3820,3835,3839,3842,3868,3874,3880,3922,3925,3929,3932,3938,4109,4112,4118,4122,4125,4288,4291,4295,4298,4304,4310,4320,4326,4332,4336,4339,4342,4345,4358,4368,4374,4380,4384,4387,4390,4419,4430,4433,4437,4440,4472,4475,4477,4483,4485,4487,4513],[3743,3744,3738],"h1",{"id":3745},"dependency-vulnerability-management-keeping-third-party-code-safe",[15,3747,3748,3749,3752],{},"Every package in your ",[33,3750,3751],{},"node_modules"," directory is code you did not write and code you are responsible for. That directory on a typical Node.js project contains hundreds or thousands of packages — a tangled graph of direct and transitive dependencies, most of which your team has never reviewed. Some of those packages have known vulnerabilities. Some will develop vulnerabilities after you install them. Managing this effectively is a non-trivial ongoing responsibility.",[15,3754,3755],{},"Here is how I think about dependency security in a way that is sustainable.",[22,3757,3759],{"id":3758},"understanding-your-attack-surface","Understanding Your Attack Surface",[15,3761,3762],{},"Before you can manage dependencies, understand what you have. Run an audit:",[56,3764,3766],{"className":1520,"code":3765,"language":1522,"meta":61,"style":61},"npm audit\n",[33,3767,3768],{"__ignoreMap":61},[65,3769,3770,3773],{"class":67,"line":68},[65,3771,3772],{"class":1534},"npm",[65,3774,3775],{"class":1410}," audit\n",[15,3777,3778],{},"This queries the npm advisory database against your installed packages and reports known vulnerabilities with severity levels. The output looks like:",[56,3780,3785],{"className":3781,"code":3783,"language":3784},[3782],"language-text","found 3 vulnerabilities (1 low, 1 moderate, 1 high)\n","text",[33,3786,3783],{"__ignoreMap":61},[15,3788,3789],{},"Follow up with:",[56,3791,3793],{"className":1520,"code":3792,"language":1522,"meta":61,"style":61},"npm audit --json | jq '.vulnerabilities | keys'\n",[33,3794,3795],{"__ignoreMap":61},[65,3796,3797,3799,3802,3805,3807,3810],{"class":67,"line":68},[65,3798,3772],{"class":1534},[65,3800,3801],{"class":1410}," audit",[65,3803,3804],{"class":1574}," --json",[65,3806,1548],{"class":1541},[65,3808,3809],{"class":1534}," jq",[65,3811,3812],{"class":1410}," '.vulnerabilities | keys'\n",[15,3814,3815,3816,3819],{},"To get a list of affected packages. For each vulnerability, ",[33,3817,3818],{},"npm audit"," reports the severity, the affected package, the vulnerability description, the path in your dependency tree, and whether a fix is available.",[15,3821,3822,3823,3826,3827,3830,3831,3834],{},"The numbers that matter are ",[33,3824,3825],{},"high"," and ",[33,3828,3829],{},"critical"," severity vulnerabilities with available fixes. ",[33,3832,3833],{},"low"," severity vulnerabilities in deeply transitive dependencies where no fix is available are background noise — important to know about but not necessarily actionable today.",[22,3836,3838],{"id":3837},"running-npm-audit-in-ci","Running npm audit in CI",[15,3840,3841],{},"Every CI pipeline should include a dependency audit:",[56,3843,3845],{"className":1370,"code":3844,"language":1372,"meta":61,"style":61},"- name: Security audit\n run: npm audit --audit-level=high\n",[33,3846,3847,3859],{"__ignoreMap":61},[65,3848,3849,3852,3854,3856],{"class":67,"line":68},[65,3850,3851],{"class":1389},"- ",[65,3853,980],{"class":1385},[65,3855,1407],{"class":1389},[65,3857,3858],{"class":1410},"Security audit\n",[65,3860,3861,3863,3865],{"class":67,"line":74},[65,3862,1416],{"class":1385},[65,3864,1407],{"class":1389},[65,3866,3867],{"class":1410},"npm audit --audit-level=high\n",[15,3869,3870,3873],{},[33,3871,3872],{},"--audit-level=high"," fails the build only for high and critical severity vulnerabilities. This is the right threshold to start with — it catches serious issues without generating noise from low-severity findings that may not be fixable.",[15,3875,3876,3877,3879],{},"The problem with ",[33,3878,3818],{}," is false positives and unavoidable vulnerabilities. A vulnerability in a package you use may only be exploitable in a different context than your usage, or the fix may not be available yet, or the fix may introduce breaking changes. For these cases, use an audit configuration file:",[56,3881,3885],{"className":3882,"code":3883,"language":3884,"meta":61,"style":61},"language-json shiki shiki-themes github-dark","// .npmrc or audit-level configuration\n{\n \"auditLevel\": \"high\",\n \"ignore\": []\n}\n","json",[33,3886,3887,3892,3897,3910,3918],{"__ignoreMap":61},[65,3888,3889],{"class":67,"line":68},[65,3890,3891],{"class":1379},"// .npmrc or audit-level configuration\n",[65,3893,3894],{"class":67,"line":74},[65,3895,3896],{"class":1389},"{\n",[65,3898,3899,3902,3904,3907],{"class":67,"line":80},[65,3900,3901],{"class":1574}," \"auditLevel\"",[65,3903,1407],{"class":1389},[65,3905,3906],{"class":1410},"\"high\"",[65,3908,3909],{"class":1389},",\n",[65,3911,3912,3915],{"class":67,"line":86},[65,3913,3914],{"class":1574}," \"ignore\"",[65,3916,3917],{"class":1389},": []\n",[65,3919,3920],{"class":67,"line":92},[65,3921,1952],{"class":1389},[15,3923,3924],{},"For specific CVEs you have evaluated and determined do not affect your usage, document them in your audit CI step and skip those specific advisories — but document why you are skipping them. \"This vulnerability is in the server-side rendering path of package X, and we only use it client-side\" is a documented risk acceptance. \"This is annoying so we are ignoring it\" is not.",[22,3926,3928],{"id":3927},"dependabot-automated-dependency-updates","Dependabot: Automated Dependency Updates",[15,3930,3931],{},"Manual dependency updates do not happen consistently. A package has a security update available, someone notes it, it goes on the backlog, the backlog is never prioritized, six months later the vulnerability is exploited. This cycle is common and preventable.",[15,3933,3934,3935,2894],{},"GitHub Dependabot automatically creates pull requests for dependency updates. Configure it in ",[33,3936,3937],{},".github/dependabot.yml",[56,3939,3941],{"className":1370,"code":3940,"language":1372,"meta":61,"style":61},"version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n day: \"monday\"\n time: \"09:00\"\n open-pull-requests-limit: 10\n labels:\n - \"dependencies\"\n - \"automated\"\n reviewers:\n - \"your-team\"\n # Group related updates to reduce PR noise\n groups:\n production-dependencies:\n dependency-type: \"production\"\n development-dependencies:\n dependency-type: \"development\"\n",[33,3942,3943,3953,3960,3972,3982,3989,3999,4009,4019,4029,4036,4043,4050,4057,4064,4069,4076,4083,4093,4100],{"__ignoreMap":61},[65,3944,3945,3948,3950],{"class":67,"line":68},[65,3946,3947],{"class":1385},"version",[65,3949,1407],{"class":1389},[65,3951,3952],{"class":1574},"2\n",[65,3954,3955,3958],{"class":67,"line":74},[65,3956,3957],{"class":1385},"updates",[65,3959,1390],{"class":1389},[65,3961,3962,3964,3967,3969],{"class":67,"line":80},[65,3963,1402],{"class":1389},[65,3965,3966],{"class":1385},"package-ecosystem",[65,3968,1407],{"class":1389},[65,3970,3971],{"class":1410},"\"npm\"\n",[65,3973,3974,3977,3979],{"class":67,"line":86},[65,3975,3976],{"class":1385}," directory",[65,3978,1407],{"class":1389},[65,3980,3981],{"class":1410},"\"/\"\n",[65,3983,3984,3987],{"class":67,"line":92},[65,3985,3986],{"class":1385}," schedule",[65,3988,1390],{"class":1389},[65,3990,3991,3994,3996],{"class":67,"line":98},[65,3992,3993],{"class":1385}," interval",[65,3995,1407],{"class":1389},[65,3997,3998],{"class":1410},"\"weekly\"\n",[65,4000,4001,4004,4006],{"class":67,"line":104},[65,4002,4003],{"class":1385}," day",[65,4005,1407],{"class":1389},[65,4007,4008],{"class":1410},"\"monday\"\n",[65,4010,4011,4014,4016],{"class":67,"line":110},[65,4012,4013],{"class":1385}," time",[65,4015,1407],{"class":1389},[65,4017,4018],{"class":1410},"\"09:00\"\n",[65,4020,4021,4024,4026],{"class":67,"line":367},[65,4022,4023],{"class":1385}," open-pull-requests-limit",[65,4025,1407],{"class":1389},[65,4027,4028],{"class":1574},"10\n",[65,4030,4031,4034],{"class":67,"line":431},[65,4032,4033],{"class":1385}," labels",[65,4035,1390],{"class":1389},[65,4037,4038,4040],{"class":67,"line":713},[65,4039,1402],{"class":1389},[65,4041,4042],{"class":1410},"\"dependencies\"\n",[65,4044,4045,4047],{"class":67,"line":1043},[65,4046,1402],{"class":1389},[65,4048,4049],{"class":1410},"\"automated\"\n",[65,4051,4052,4055],{"class":67,"line":1049},[65,4053,4054],{"class":1385}," reviewers",[65,4056,1390],{"class":1389},[65,4058,4059,4061],{"class":67,"line":1190},[65,4060,1402],{"class":1389},[65,4062,4063],{"class":1410},"\"your-team\"\n",[65,4065,4066],{"class":67,"line":1196},[65,4067,4068],{"class":1379}," # Group related updates to reduce PR noise\n",[65,4070,4071,4074],{"class":67,"line":1201},[65,4072,4073],{"class":1385}," groups",[65,4075,1390],{"class":1389},[65,4077,4078,4081],{"class":67,"line":1207},[65,4079,4080],{"class":1385}," production-dependencies",[65,4082,1390],{"class":1389},[65,4084,4085,4088,4090],{"class":67,"line":1213},[65,4086,4087],{"class":1385}," dependency-type",[65,4089,1407],{"class":1389},[65,4091,4092],{"class":1410},"\"production\"\n",[65,4094,4095,4098],{"class":67,"line":1219},[65,4096,4097],{"class":1385}," development-dependencies",[65,4099,1390],{"class":1389},[65,4101,4102,4104,4106],{"class":67,"line":3094},[65,4103,4087],{"class":1385},[65,4105,1407],{"class":1389},[65,4107,4108],{"class":1410},"\"development\"\n",[15,4110,4111],{},"Dependabot creates separate PRs for each dependency update. Your CI runs on these PRs. If tests pass, the PR can be merged. If they fail, the update has a compatibility issue that needs review.",[15,4113,185,4114,4117],{},[33,4115,4116],{},"groups"," configuration batches related updates into single PRs, reducing PR noise. Production dependencies and development dependencies are grouped separately so you can apply different review standards — production dependency updates warrant more careful review than a development tool update.",[22,4119,4121],{"id":4120},"renovate-as-an-alternative","Renovate as an Alternative",[15,4123,4124],{},"Renovate (by Mend, formerly WhiteSource) is a more configurable alternative to Dependabot. It supports grouping updates by category, scheduling automerge for specific package types, and detecting when updated packages have new major versions that require manual review.",[56,4126,4128],{"className":3882,"code":4127,"language":3884,"meta":61,"style":61},"// renovate.json\n{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\"config:recommended\"],\n \"schedule\": [\"on monday\"],\n \"packageRules\": [\n {\n \"matchUpdateTypes\": [\"patch\"],\n \"matchCurrentVersion\": \"!/^0/\",\n \"automerge\": true\n },\n {\n \"matchPackagePatterns\": [\"*\"],\n \"matchUpdateTypes\": [\"major\"],\n \"labels\": [\"major-update\"],\n \"reviewersFromCodeOwners\": true\n }\n ]\n}\n",[33,4129,4130,4135,4139,4151,4165,4177,4185,4189,4201,4213,4223,4227,4231,4243,4254,4266,4275,4279,4284],{"__ignoreMap":61},[65,4131,4132],{"class":67,"line":68},[65,4133,4134],{"class":1379},"// renovate.json\n",[65,4136,4137],{"class":67,"line":74},[65,4138,3896],{"class":1389},[65,4140,4141,4144,4146,4149],{"class":67,"line":80},[65,4142,4143],{"class":1574}," \"$schema\"",[65,4145,1407],{"class":1389},[65,4147,4148],{"class":1410},"\"https://docs.renovatebot.com/renovate-schema.json\"",[65,4150,3909],{"class":1389},[65,4152,4153,4156,4159,4162],{"class":67,"line":86},[65,4154,4155],{"class":1574}," \"extends\"",[65,4157,4158],{"class":1389},": [",[65,4160,4161],{"class":1410},"\"config:recommended\"",[65,4163,4164],{"class":1389},"],\n",[65,4166,4167,4170,4172,4175],{"class":67,"line":92},[65,4168,4169],{"class":1574}," \"schedule\"",[65,4171,4158],{"class":1389},[65,4173,4174],{"class":1410},"\"on monday\"",[65,4176,4164],{"class":1389},[65,4178,4179,4182],{"class":67,"line":98},[65,4180,4181],{"class":1574}," \"packageRules\"",[65,4183,4184],{"class":1389},": [\n",[65,4186,4187],{"class":67,"line":104},[65,4188,1801],{"class":1389},[65,4190,4191,4194,4196,4199],{"class":67,"line":110},[65,4192,4193],{"class":1574}," \"matchUpdateTypes\"",[65,4195,4158],{"class":1389},[65,4197,4198],{"class":1410},"\"patch\"",[65,4200,4164],{"class":1389},[65,4202,4203,4206,4208,4211],{"class":67,"line":367},[65,4204,4205],{"class":1574}," \"matchCurrentVersion\"",[65,4207,1407],{"class":1389},[65,4209,4210],{"class":1410},"\"!/^0/\"",[65,4212,3909],{"class":1389},[65,4214,4215,4218,4220],{"class":67,"line":431},[65,4216,4217],{"class":1574}," \"automerge\"",[65,4219,1407],{"class":1389},[65,4221,4222],{"class":1574},"true\n",[65,4224,4225],{"class":67,"line":713},[65,4226,2790],{"class":1389},[65,4228,4229],{"class":67,"line":1043},[65,4230,1801],{"class":1389},[65,4232,4233,4236,4238,4241],{"class":67,"line":1049},[65,4234,4235],{"class":1574}," \"matchPackagePatterns\"",[65,4237,4158],{"class":1389},[65,4239,4240],{"class":1410},"\"*\"",[65,4242,4164],{"class":1389},[65,4244,4245,4247,4249,4252],{"class":67,"line":1190},[65,4246,4193],{"class":1574},[65,4248,4158],{"class":1389},[65,4250,4251],{"class":1410},"\"major\"",[65,4253,4164],{"class":1389},[65,4255,4256,4259,4261,4264],{"class":67,"line":1196},[65,4257,4258],{"class":1574}," \"labels\"",[65,4260,4158],{"class":1389},[65,4262,4263],{"class":1410},"\"major-update\"",[65,4265,4164],{"class":1389},[65,4267,4268,4271,4273],{"class":67,"line":1201},[65,4269,4270],{"class":1574}," \"reviewersFromCodeOwners\"",[65,4272,1407],{"class":1389},[65,4274,4222],{"class":1574},[65,4276,4277],{"class":67,"line":1207},[65,4278,1862],{"class":1389},[65,4280,4281],{"class":67,"line":1213},[65,4282,4283],{"class":1389}," ]\n",[65,4285,4286],{"class":67,"line":1219},[65,4287,1952],{"class":1389},[15,4289,4290],{},"This configuration auto-merges patch updates for stable packages (non-v0) when CI passes, while requiring manual review for major updates. This reduces the overhead of dependency maintenance significantly — patch updates (usually bug fixes and security patches) merge automatically, while major updates that might have breaking changes get reviewed.",[22,4292,4294],{"id":4293},"evaluating-dependencies-before-adding-them","Evaluating Dependencies Before Adding Them",[15,4296,4297],{},"Vulnerability management is easier when you are selective about what you add. Before adding a new dependency, evaluate:",[15,4299,4300,4303],{},[124,4301,4302],{},"Maintenance status"," — is the package actively maintained? When was the last release? Are there open issues with no response? An unmaintained package will not receive security patches.",[15,4305,4306,4309],{},[124,4307,4308],{},"Popularity and community"," — a popular package with a large user base is more likely to have vulnerabilities discovered and patched quickly. Obscure packages with few users may have vulnerabilities that nobody has found or reported yet.",[15,4311,4312,4315,4316,4319],{},[124,4313,4314],{},"Dependencies of the dependency"," — installing one package installs all of its transitive dependencies. ",[33,4317,4318],{},"npm ls"," shows the dependency tree. Adding a package that pulls in 50 transitive dependencies adds 50 packages to your attack surface.",[15,4321,4322,4325],{},[124,4323,4324],{},"License"," — not security-related, but worth checking. MIT and Apache 2.0 are safe for most applications. GPL and LGPL have implications for open-source distribution.",[15,4327,4328,4331],{},[124,4329,4330],{},"Can you write it yourself?"," — for simple utilities (left-pad famously illustrates this), consider whether the package is simpler to implement directly. Fewer dependencies is fewer vulnerabilities.",[22,4333,4335],{"id":4334},"supply-chain-attacks","Supply Chain Attacks",[15,4337,4338],{},"Beyond known vulnerabilities, supply chain attacks are an increasing threat. A malicious actor takes over a popular package (by compromising a maintainer's account, registering a typosquatted package name, or injecting malicious code into the build process of an open-source project) and publishes a version containing malicious code. Thousands of applications install the update and execute the malicious code.",[15,4340,4341],{},"This has happened with real packages: event-stream (2018), ua-parser-js (2021), node-ipc (2022), and several others. The impact can be credential theft, data exfiltration, or in the case of node-ipc, intentional data destruction.",[15,4343,4344],{},"Mitigation strategies:",[15,4346,4347,4350,4351,1998,4354,4357],{},[124,4348,4349],{},"Pin to exact versions."," Use a lockfile (",[33,4352,4353],{},"package-lock.json",[33,4355,4356],{},"yarn.lock",") and commit it. Every install gets exactly the version that was tested.",[15,4359,4360,4363,4364,4367],{},[124,4361,4362],{},"Verify integrity."," npm's lockfile includes integrity checksums. ",[33,4365,4366],{},"npm ci"," verifies integrity on install and fails if checksums do not match.",[15,4369,4370,4373],{},[124,4371,4372],{},"Monitor for unusual behavior."," Tools like Socket Security analyze package changes and flag packages that add new network calls, file system access, or post-install scripts in new versions.",[15,4375,4376,4379],{},[124,4377,4378],{},"Review dependency changes in PRs."," When Dependabot creates a PR for a dependency update, review what changed in the package. For high-risk packages (packages with broad system access or network capabilities), check the changelog and even the diff.",[22,4381,4383],{"id":4382},"the-software-bill-of-materials-sbom","The Software Bill of Materials (SBOM)",[15,4385,4386],{},"An SBOM is a formal inventory of all components in your software, including their versions and licenses. Generating an SBOM makes it possible to quickly answer \"are any of our applications using the affected package?\" when a new vulnerability is announced.",[15,4388,4389],{},"Generate an SBOM for your application:",[56,4391,4393],{"className":1520,"code":4392,"language":1522,"meta":61,"style":61},"npm install -g @cyclonedx/cyclonedx-npm\ncyclonedx-npm --output-file sbom.json\n",[33,4394,4395,4408],{"__ignoreMap":61},[65,4396,4397,4399,4402,4405],{"class":67,"line":68},[65,4398,3772],{"class":1534},[65,4400,4401],{"class":1410}," install",[65,4403,4404],{"class":1574}," -g",[65,4406,4407],{"class":1410}," @cyclonedx/cyclonedx-npm\n",[65,4409,4410,4413,4416],{"class":67,"line":74},[65,4411,4412],{"class":1534},"cyclonedx-npm",[65,4414,4415],{"class":1574}," --output-file",[65,4417,4418],{"class":1410}," sbom.json\n",[15,4420,4421,4422,4425,4426,4429],{},"Store SBOMs as build artifacts alongside your releases. When CVE-2026-XXXXX is announced affecting ",[33,4423,4424],{},"some-package"," below version ",[33,4427,4428],{},"2.3.4",", you can query your SBOMs to find affected applications in minutes.",[15,4431,4432],{},"SBOM generation is increasingly required for government and enterprise software procurement. Including it in your build pipeline now prepares you for that requirement.",[22,4434,4436],{"id":4435},"the-practical-update-cadence","The Practical Update Cadence",[15,4438,4439],{},"Security advisories and updates cannot be ignored indefinitely. The operational cadence I recommend:",[118,4441,4442,4448,4454,4460,4466],{},[121,4443,4444,4447],{},[124,4445,4446],{},"Critical vulnerabilities with fixes:"," patch within 24-48 hours. Do not wait for a sprint cycle.",[121,4449,4450,4453],{},[124,4451,4452],{},"High severity with fixes:"," patch within one week.",[121,4455,4456,4459],{},[124,4457,4458],{},"High severity without fixes:"," document the risk, implement compensating controls if possible (WAF rules, network controls), and track the advisory for when a fix becomes available.",[121,4461,4462,4465],{},[124,4463,4464],{},"Moderate severity:"," include in next sprint cycle.",[121,4467,4468,4471],{},[124,4469,4470],{},"Low severity:"," batch quarterly with regular maintenance updates.",[15,4473,4474],{},"Automated tooling (Dependabot, Renovate) with CI validation handles the routine updates. Reserve human judgment for critical issues, breaking changes, and vulnerabilities without available fixes.",[746,4476],{},[15,4478,4479,4480,758],{},"If you want help setting up a dependency security program for your team or need a review of your current vulnerability management practices, book a session at ",[752,4481,754],{"href":754,"rel":4482},[756],[746,4484],{},[22,4486,764],{"id":763},[118,4488,4489,4495,4501,4507],{},[121,4490,4491],{},[752,4492,4494],{"href":4493},"/blog/api-security-best-practices","API Security Best Practices: Protecting Your Endpoints in Production",[121,4496,4497],{},[752,4498,4500],{"href":4499},"/blog/authentication-security-guide","Authentication Security: What to Get Right Before Your First User Logs In",[121,4502,4503],{},[752,4504,4506],{"href":4505},"/blog/csrf-protection-guide","CSRF Protection: Understanding Cross-Site Request Forgery and Stopping It",[121,4508,4509],{},[752,4510,4512],{"href":4511},"/blog/content-security-policy-guide","Content Security Policy: Stopping XSS at the Browser Level",[792,4514,4515],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":80,"depth":80,"links":4517},[4518,4519,4520,4521,4522,4523,4524,4525,4526],{"id":3758,"depth":74,"text":3759},{"id":3837,"depth":74,"text":3838},{"id":3927,"depth":74,"text":3928},{"id":4120,"depth":74,"text":4121},{"id":4293,"depth":74,"text":4294},{"id":4334,"depth":74,"text":4335},{"id":4382,"depth":74,"text":4383},{"id":4435,"depth":74,"text":4436},{"id":763,"depth":74,"text":764},"Security","Manage dependency vulnerabilities effectively — npm audit, Dependabot, Software Bill of Materials, transitive dependencies, and building a sustainable update workflow for your team.",[4530,3818],"dependency vulnerability",{},"/blog/dependency-vulnerability-management",{"title":3738,"description":4528},"blog/dependency-vulnerability-management",[4536,4527,3772,4537],"Dependencies","Supply Chain","N7bsQA5Fm_PfDyHkePPCFlIGdz0JTcdSe3fI8cxWLrE",{"id":4540,"title":4541,"author":4542,"body":4543,"category":5607,"date":809,"description":5608,"extension":811,"featured":812,"image":813,"keywords":5609,"meta":5615,"navigation":278,"path":5616,"readTime":431,"seo":5617,"stem":5618,"tags":5619,"__hash__":5624},"blog/blog/design-patterns-for-architects.md","Software Design Patterns Every Architect Should Have in Their Toolkit",{"name":9,"bio":10},{"type":12,"value":4544,"toc":5596},[4545,4549,4552,4555,4558,4560,4564,4567,4570,4771,4781,4784,4786,4790,4793,4800,4946,4953,4955,4959,4962,4976,5199,5214,5216,5220,5223,5226,5383,5390,5393,5395,5399,5402,5405,5411,5417,5424,5430,5436,5439,5442,5444,5448,5451,5454,5461,5490,5496,5499,5501,5505,5508,5546,5549,5551,5554,5556,5563,5565,5567,5593],[22,4546,4548],{"id":4547},"patterns-at-the-right-level","Patterns at the Right Level",[15,4550,4551],{},"The classic Design Patterns book by the Gang of Four catalogued 23 patterns in 1994. They're well-documented and widely taught. They're also frequently applied at the wrong level of abstraction — used as implementation tricks rather than as architectural tools.",[15,4553,4554],{},"The patterns that matter most to an architect are the ones that solve structural problems: how do you compose behavior without coupling implementations? How do you coordinate distributed transactions without a 2-phase commit? How do you ensure that database writes and event publishing don't diverge? These are architectural problems, and the patterns that address them operate at a different scale than \"how do I avoid if-else chains.\"",[15,4556,4557],{},"Here's a practitioner's view of the patterns I reach for most often as an architect.",[746,4559],{},[22,4561,4563],{"id":4562},"strategy-pattern-composing-variable-behavior","Strategy Pattern: Composing Variable Behavior",[15,4565,4566],{},"The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. At the code level, this is a pattern for avoiding switch statements and conditional chains. At the architectural level, it's a tool for making systems extensible without modification.",[15,4568,4569],{},"The architectural application: when you need to support multiple variants of a behavior that share the same interface but different implementations — payment processors, notification channels, data export formats, authentication providers — the strategy pattern lets you add new variants without touching existing code.",[56,4571,4573],{"className":1726,"code":4572,"language":1728,"meta":61,"style":61},"interface PaymentStrategy {\n charge(amount: Money, customer: Customer): Promise\u003CChargeResult>\n refund(transactionId: string, amount: Money): Promise\u003CRefundResult>\n}\n\nClass StripePaymentStrategy implements PaymentStrategy { /* ... */ }\nclass PayPalPaymentStrategy implements PaymentStrategy { /* ... */ }\nclass ACHPaymentStrategy implements PaymentStrategy { /* ... */ }\n\nClass PaymentService {\n constructor(private readonly strategy: PaymentStrategy) {}\n\n async processPayment(order: Order): Promise\u003CPaymentResult> {\n return this.strategy.charge(order.total, order.customer)\n }\n}\n",[33,4574,4575,4585,4624,4659,4663,4667,4677,4697,4714,4718,4723,4731,4735,4758,4763,4767],{"__ignoreMap":61},[65,4576,4577,4580,4583],{"class":67,"line":68},[65,4578,4579],{"class":1541},"interface",[65,4581,4582],{"class":1534}," PaymentStrategy",[65,4584,1801],{"class":1389},[65,4586,4587,4590,4592,4594,4596,4599,4601,4604,4606,4609,4611,4613,4616,4618,4621],{"class":67,"line":74},[65,4588,4589],{"class":1534}," charge",[65,4591,1783],{"class":1389},[65,4593,2903],{"class":1791},[65,4595,2894],{"class":1541},[65,4597,4598],{"class":1534}," Money",[65,4600,2900],{"class":1389},[65,4602,4603],{"class":1791},"customer",[65,4605,2894],{"class":1541},[65,4607,4608],{"class":1534}," Customer",[65,4610,876],{"class":1389},[65,4612,2894],{"class":1541},[65,4614,4615],{"class":1534}," Promise",[65,4617,2946],{"class":1389},[65,4619,4620],{"class":1534},"ChargeResult",[65,4622,4623],{"class":1389},">\n",[65,4625,4626,4629,4631,4634,4636,4638,4640,4642,4644,4646,4648,4650,4652,4654,4657],{"class":67,"line":80},[65,4627,4628],{"class":1534}," refund",[65,4630,1783],{"class":1389},[65,4632,4633],{"class":1791},"transactionId",[65,4635,2894],{"class":1541},[65,4637,2897],{"class":1574},[65,4639,2900],{"class":1389},[65,4641,2903],{"class":1791},[65,4643,2894],{"class":1541},[65,4645,4598],{"class":1534},[65,4647,876],{"class":1389},[65,4649,2894],{"class":1541},[65,4651,4615],{"class":1534},[65,4653,2946],{"class":1389},[65,4655,4656],{"class":1534},"RefundResult",[65,4658,4623],{"class":1389},[65,4660,4661],{"class":67,"line":86},[65,4662,1952],{"class":1389},[65,4664,4665],{"class":67,"line":92},[65,4666,279],{"emptyLinePlaceholder":278},[65,4668,4669,4672,4675],{"class":67,"line":98},[65,4670,4671],{"class":1389},"Class StripePaymentStrategy implements PaymentStrategy { ",[65,4673,4674],{"class":1379},"/* ... */",[65,4676,1862],{"class":1389},[65,4678,4679,4682,4685,4688,4690,4693,4695],{"class":67,"line":104},[65,4680,4681],{"class":1541},"class",[65,4683,4684],{"class":1534}," PayPalPaymentStrategy",[65,4686,4687],{"class":1541}," implements",[65,4689,4582],{"class":1534},[65,4691,4692],{"class":1389}," { ",[65,4694,4674],{"class":1379},[65,4696,1862],{"class":1389},[65,4698,4699,4701,4704,4706,4708,4710,4712],{"class":67,"line":110},[65,4700,4681],{"class":1541},[65,4702,4703],{"class":1534}," ACHPaymentStrategy",[65,4705,4687],{"class":1541},[65,4707,4582],{"class":1534},[65,4709,4692],{"class":1389},[65,4711,4674],{"class":1379},[65,4713,1862],{"class":1389},[65,4715,4716],{"class":67,"line":367},[65,4717,279],{"emptyLinePlaceholder":278},[65,4719,4720],{"class":67,"line":431},[65,4721,4722],{"class":1389},"Class PaymentService {\n",[65,4724,4725,4728],{"class":67,"line":713},[65,4726,4727],{"class":1534}," constructor",[65,4729,4730],{"class":1389},"(private readonly strategy: PaymentStrategy) {}\n",[65,4732,4733],{"class":67,"line":1043},[65,4734,279],{"emptyLinePlaceholder":278},[65,4736,4737,4740,4743,4746,4749,4751,4754,4756],{"class":67,"line":1049},[65,4738,4739],{"class":1389}," async ",[65,4741,4742],{"class":1534},"processPayment",[65,4744,4745],{"class":1389},"(order: Order): ",[65,4747,4748],{"class":1574},"Promise",[65,4750,2946],{"class":1541},[65,4752,4753],{"class":1389},"PaymentResult",[65,4755,1812],{"class":1541},[65,4757,1801],{"class":1389},[65,4759,4760],{"class":67,"line":1190},[65,4761,4762],{"class":1389}," return this.strategy.charge(order.total, order.customer)\n",[65,4764,4765],{"class":67,"line":1196},[65,4766,1862],{"class":1389},[65,4768,4769],{"class":67,"line":1201},[65,4770,1952],{"class":1389},[15,4772,185,4773,4776,4777,4780],{},[33,4774,4775],{},"PaymentService"," doesn't know about Stripe or PayPal. Adding a new payment processor requires implementing the ",[33,4778,4779],{},"PaymentStrategy"," interface — no changes to existing code. This is the Open/Closed Principle made concrete.",[15,4782,4783],{},"Architecturally, Strategy is how you keep core business logic stable while allowing integration points to vary. Every external service your domain interacts with is a candidate for a strategy interface.",[746,4785],{},[22,4787,4789],{"id":4788},"factory-pattern-managing-object-creation-complexity","Factory Pattern: Managing Object Creation Complexity",[15,4791,4792],{},"The Factory pattern centralizes object creation logic, hiding the complexity of instantiation from the calling code. At the architectural level, it's how you manage the creation of complex objects whose construction requires decisions based on runtime conditions.",[15,4794,4795,4796,4799],{},"The architectural application: when the \"right\" implementation to create depends on context — configuration, environment, request parameters — a factory centralizes that decision rather than scattering ",[33,4797,4798],{},"if (env === 'production') { ... }"," conditionals throughout the codebase.",[56,4801,4803],{"className":1726,"code":4802,"language":1728,"meta":61,"style":61},"class StorageAdapterFactory {\n create(config: StorageConfig): StorageAdapter {\n switch (config.provider) {\n case 's3': return new S3StorageAdapter(config.s3)\n case 'gcs': return new GCSStorageAdapter(config.gcs)\n case 'azure-blob': return new AzureBlobStorageAdapter(config.azure)\n default: throw new Error(`Unknown storage provider: ${config.provider}`)\n }\n }\n}\n",[33,4804,4805,4814,4838,4846,4866,4885,4904,4934,4938,4942],{"__ignoreMap":61},[65,4806,4807,4809,4812],{"class":67,"line":68},[65,4808,4681],{"class":1541},[65,4810,4811],{"class":1534}," StorageAdapterFactory",[65,4813,1801],{"class":1389},[65,4815,4816,4819,4821,4824,4826,4829,4831,4833,4836],{"class":67,"line":74},[65,4817,4818],{"class":1534}," create",[65,4820,1783],{"class":1389},[65,4822,4823],{"class":1791},"config",[65,4825,2894],{"class":1541},[65,4827,4828],{"class":1534}," StorageConfig",[65,4830,876],{"class":1389},[65,4832,2894],{"class":1541},[65,4834,4835],{"class":1534}," StorageAdapter",[65,4837,1801],{"class":1389},[65,4839,4840,4843],{"class":67,"line":80},[65,4841,4842],{"class":1541}," switch",[65,4844,4845],{"class":1389}," (config.provider) {\n",[65,4847,4848,4851,4854,4856,4858,4860,4863],{"class":67,"line":86},[65,4849,4850],{"class":1541}," case",[65,4852,4853],{"class":1410}," 's3'",[65,4855,1407],{"class":1389},[65,4857,3083],{"class":1541},[65,4859,1744],{"class":1541},[65,4861,4862],{"class":1534}," S3StorageAdapter",[65,4864,4865],{"class":1389},"(config.s3)\n",[65,4867,4868,4870,4873,4875,4877,4879,4882],{"class":67,"line":92},[65,4869,4850],{"class":1541},[65,4871,4872],{"class":1410}," 'gcs'",[65,4874,1407],{"class":1389},[65,4876,3083],{"class":1541},[65,4878,1744],{"class":1541},[65,4880,4881],{"class":1534}," GCSStorageAdapter",[65,4883,4884],{"class":1389},"(config.gcs)\n",[65,4886,4887,4889,4892,4894,4896,4898,4901],{"class":67,"line":98},[65,4888,4850],{"class":1541},[65,4890,4891],{"class":1410}," 'azure-blob'",[65,4893,1407],{"class":1389},[65,4895,3083],{"class":1541},[65,4897,1744],{"class":1541},[65,4899,4900],{"class":1534}," AzureBlobStorageAdapter",[65,4902,4903],{"class":1389},"(config.azure)\n",[65,4905,4906,4909,4911,4914,4916,4918,4920,4923,4925,4927,4930,4932],{"class":67,"line":104},[65,4907,4908],{"class":1541}," default",[65,4910,1407],{"class":1389},[65,4912,4913],{"class":1541},"throw",[65,4915,1744],{"class":1541},[65,4917,3112],{"class":1534},[65,4919,1783],{"class":1389},[65,4921,4922],{"class":1410},"`Unknown storage provider: ${",[65,4924,4823],{"class":1389},[65,4926,758],{"class":1410},[65,4928,4929],{"class":1389},"provider",[65,4931,1854],{"class":1410},[65,4933,1857],{"class":1389},[65,4935,4936],{"class":67,"line":110},[65,4937,1862],{"class":1389},[65,4939,4940],{"class":67,"line":367},[65,4941,1862],{"class":1389},[65,4943,4944],{"class":67,"line":431},[65,4945,1952],{"class":1389},[15,4947,4948,4949,4952],{},"The factory is the one place that knows about all the concrete implementations. Every other part of the system works against the ",[33,4950,4951],{},"StorageAdapter"," interface. Switching storage providers is a configuration change, not a code change.",[746,4954],{},[22,4956,4958],{"id":4957},"observer-pattern-decoupled-event-handling","Observer Pattern: Decoupled Event Handling",[15,4960,4961],{},"The Observer pattern lets objects subscribe to events published by another object without the publisher knowing about the subscribers. At the architectural level, this is the foundation of event-driven design within a single bounded context.",[15,4963,4964,4965,4968,4969,4971,4972,4975],{},"The architectural application: domain events. When an ",[33,4966,4967],{},"Order"," is placed, multiple things might need to happen — inventory reservation, notification sending, analytics tracking, fraud checking. If the ",[33,4970,4967],{}," aggregate directly calls each of these, it becomes coupled to every consumer. Observer (via domain events) lets the aggregate publish ",[33,4973,4974],{},"OrderPlaced"," and delegate the reaction to whoever cares.",[56,4977,4979],{"className":1726,"code":4978,"language":1728,"meta":61,"style":61},"class Order {\n private events: DomainEvent[] = []\n\n place(): void {\n // ... Business logic\n this.events.push(new OrderPlaced(this.id, this.customerId, this.items))\n }\n\n pullEvents(): DomainEvent[] {\n const events = [...this.events]\n this.events = []\n return events\n }\n}\n\n// In the application layer after saving the order:\nconst events = order.pullEvents()\nfor (const event of events) {\n await this.eventBus.publish(event)\n}\n",[33,4980,4981,4990,5011,5015,5030,5035,5072,5076,5080,5094,5112,5123,5131,5135,5139,5143,5148,5164,5180,5195],{"__ignoreMap":61},[65,4982,4983,4985,4988],{"class":67,"line":68},[65,4984,4681],{"class":1541},[65,4986,4987],{"class":1534}," Order",[65,4989,1801],{"class":1389},[65,4991,4992,4995,4998,5000,5003,5006,5008],{"class":67,"line":74},[65,4993,4994],{"class":1541}," private",[65,4996,4997],{"class":1791}," events",[65,4999,2894],{"class":1541},[65,5001,5002],{"class":1534}," DomainEvent",[65,5004,5005],{"class":1389},"[] ",[65,5007,2937],{"class":1541},[65,5009,5010],{"class":1389}," []\n",[65,5012,5013],{"class":67,"line":80},[65,5014,279],{"emptyLinePlaceholder":278},[65,5016,5017,5020,5023,5025,5028],{"class":67,"line":86},[65,5018,5019],{"class":1534}," place",[65,5021,5022],{"class":1389},"()",[65,5024,2894],{"class":1541},[65,5026,5027],{"class":1574}," void",[65,5029,1801],{"class":1389},[65,5031,5032],{"class":67,"line":92},[65,5033,5034],{"class":1379}," // ... Business logic\n",[65,5036,5037,5040,5043,5046,5048,5051,5054,5056,5059,5062,5064,5067,5069],{"class":67,"line":98},[65,5038,5039],{"class":1574}," this",[65,5041,5042],{"class":1389},".events.",[65,5044,5045],{"class":1534},"push",[65,5047,1783],{"class":1389},[65,5049,5050],{"class":1541},"new",[65,5052,5053],{"class":1534}," OrderPlaced",[65,5055,1783],{"class":1389},[65,5057,5058],{"class":1574},"this",[65,5060,5061],{"class":1389},".id, ",[65,5063,5058],{"class":1574},[65,5065,5066],{"class":1389},".customerId, ",[65,5068,5058],{"class":1574},[65,5070,5071],{"class":1389},".items))\n",[65,5073,5074],{"class":67,"line":104},[65,5075,1862],{"class":1389},[65,5077,5078],{"class":67,"line":110},[65,5079,279],{"emptyLinePlaceholder":278},[65,5081,5082,5085,5087,5089,5091],{"class":67,"line":367},[65,5083,5084],{"class":1534}," pullEvents",[65,5086,5022],{"class":1389},[65,5088,2894],{"class":1541},[65,5090,5002],{"class":1534},[65,5092,5093],{"class":1389},"[] {\n",[65,5095,5096,5098,5100,5102,5104,5107,5109],{"class":67,"line":431},[65,5097,1932],{"class":1541},[65,5099,4997],{"class":1574},[65,5101,1741],{"class":1541},[65,5103,3596],{"class":1389},[65,5105,5106],{"class":1541},"...",[65,5108,5058],{"class":1574},[65,5110,5111],{"class":1389},".events]\n",[65,5113,5114,5116,5119,5121],{"class":67,"line":713},[65,5115,5039],{"class":1574},[65,5117,5118],{"class":1389},".events ",[65,5120,2937],{"class":1541},[65,5122,5010],{"class":1389},[65,5124,5125,5128],{"class":67,"line":1043},[65,5126,5127],{"class":1541}," return",[65,5129,5130],{"class":1389}," events\n",[65,5132,5133],{"class":67,"line":1049},[65,5134,1862],{"class":1389},[65,5136,5137],{"class":67,"line":1190},[65,5138,1952],{"class":1389},[65,5140,5141],{"class":67,"line":1196},[65,5142,279],{"emptyLinePlaceholder":278},[65,5144,5145],{"class":67,"line":1201},[65,5146,5147],{"class":1379},"// In the application layer after saving the order:\n",[65,5149,5150,5152,5154,5156,5159,5162],{"class":67,"line":1207},[65,5151,1735],{"class":1541},[65,5153,4997],{"class":1574},[65,5155,1741],{"class":1541},[65,5157,5158],{"class":1389}," order.",[65,5160,5161],{"class":1534},"pullEvents",[65,5163,1909],{"class":1389},[65,5165,5166,5168,5170,5172,5175,5177],{"class":67,"line":1213},[65,5167,1914],{"class":1541},[65,5169,538],{"class":1389},[65,5171,1735],{"class":1541},[65,5173,5174],{"class":1574}," event",[65,5176,1924],{"class":1541},[65,5178,5179],{"class":1389}," events) {\n",[65,5181,5182,5184,5186,5189,5192],{"class":67,"line":1219},[65,5183,1900],{"class":1541},[65,5185,5039],{"class":1574},[65,5187,5188],{"class":1389},".eventBus.",[65,5190,5191],{"class":1534},"publish",[65,5193,5194],{"class":1389},"(event)\n",[65,5196,5197],{"class":67,"line":3094},[65,5198,1952],{"class":1389},[15,5200,185,5201,5203,5204,5207,5208,5210,5211,5213],{},[33,5202,4967],{}," aggregate is decoupled from every downstream effect. Adding a new consumer (a ",[33,5205,5206],{},"FraudDetectionService"," that reacts to ",[33,5209,4974],{},") requires no changes to the ",[33,5212,4967],{}," aggregate.",[746,5215],{},[22,5217,5219],{"id":5218},"repository-pattern-abstracting-data-access","Repository Pattern: Abstracting Data Access",[15,5221,5222],{},"The Repository pattern provides a collection-like interface for accessing domain objects, hiding the persistence implementation from the domain and application layers.",[15,5224,5225],{},"This is architecturally significant because it's one of the key enablers of clean and hexagonal architecture. The repository interface is defined in terms of domain concepts, not database concepts:",[56,5227,5229],{"className":1726,"code":5228,"language":1728,"meta":61,"style":61},"interface OrderRepository {\n findById(id: OrderId): Promise\u003COrder | null>\n findByCustomer(customerId: CustomerId): Promise\u003COrder[]>\n findPendingOlderThan(date: Date): Promise\u003COrder[]>\n save(order: Order): Promise\u003Cvoid>\n delete(order: Order): Promise\u003Cvoid>\n}\n",[33,5230,5231,5240,5272,5300,5327,5354,5379],{"__ignoreMap":61},[65,5232,5233,5235,5238],{"class":67,"line":68},[65,5234,4579],{"class":1541},[65,5236,5237],{"class":1534}," OrderRepository",[65,5239,1801],{"class":1389},[65,5241,5242,5245,5247,5250,5252,5255,5257,5259,5261,5263,5265,5267,5270],{"class":67,"line":74},[65,5243,5244],{"class":1534}," findById",[65,5246,1783],{"class":1389},[65,5248,5249],{"class":1791},"id",[65,5251,2894],{"class":1541},[65,5253,5254],{"class":1534}," OrderId",[65,5256,876],{"class":1389},[65,5258,2894],{"class":1541},[65,5260,4615],{"class":1534},[65,5262,2946],{"class":1389},[65,5264,4967],{"class":1534},[65,5266,1548],{"class":1541},[65,5268,5269],{"class":1574}," null",[65,5271,4623],{"class":1389},[65,5273,5274,5277,5279,5282,5284,5287,5289,5291,5293,5295,5297],{"class":67,"line":80},[65,5275,5276],{"class":1534}," findByCustomer",[65,5278,1783],{"class":1389},[65,5280,5281],{"class":1791},"customerId",[65,5283,2894],{"class":1541},[65,5285,5286],{"class":1534}," CustomerId",[65,5288,876],{"class":1389},[65,5290,2894],{"class":1541},[65,5292,4615],{"class":1534},[65,5294,2946],{"class":1389},[65,5296,4967],{"class":1534},[65,5298,5299],{"class":1389},"[]>\n",[65,5301,5302,5305,5307,5310,5312,5315,5317,5319,5321,5323,5325],{"class":67,"line":86},[65,5303,5304],{"class":1534}," findPendingOlderThan",[65,5306,1783],{"class":1389},[65,5308,5309],{"class":1791},"date",[65,5311,2894],{"class":1541},[65,5313,5314],{"class":1534}," Date",[65,5316,876],{"class":1389},[65,5318,2894],{"class":1541},[65,5320,4615],{"class":1534},[65,5322,2946],{"class":1389},[65,5324,4967],{"class":1534},[65,5326,5299],{"class":1389},[65,5328,5329,5332,5334,5337,5339,5341,5343,5345,5347,5349,5352],{"class":67,"line":92},[65,5330,5331],{"class":1534}," save",[65,5333,1783],{"class":1389},[65,5335,5336],{"class":1791},"order",[65,5338,2894],{"class":1541},[65,5340,4987],{"class":1534},[65,5342,876],{"class":1389},[65,5344,2894],{"class":1541},[65,5346,4615],{"class":1534},[65,5348,2946],{"class":1389},[65,5350,5351],{"class":1574},"void",[65,5353,4623],{"class":1389},[65,5355,5356,5359,5361,5363,5365,5367,5369,5371,5373,5375,5377],{"class":67,"line":98},[65,5357,5358],{"class":1534}," delete",[65,5360,1783],{"class":1389},[65,5362,5336],{"class":1791},[65,5364,2894],{"class":1541},[65,5366,4987],{"class":1534},[65,5368,876],{"class":1389},[65,5370,2894],{"class":1541},[65,5372,4615],{"class":1534},[65,5374,2946],{"class":1389},[65,5376,5351],{"class":1574},[65,5378,4623],{"class":1389},[65,5380,5381],{"class":67,"line":104},[65,5382,1952],{"class":1389},[15,5384,5385,5386,5389],{},"The domain defines what it needs. The infrastructure implements it. The domain never calls Prisma, or ActiveRecord, or raw SQL. It calls ",[33,5387,5388],{},"orderRepo.findPendingOlderThan(date)"," and works with the result.",[15,5391,5392],{},"The architectural benefit: swap Prisma for Drizzle, or PostgreSQL for DynamoDB — the domain code doesn't change. The repository adapter changes; the domain is untouched.",[746,5394],{},[22,5396,5398],{"id":5397},"saga-pattern-coordinating-distributed-transactions","Saga Pattern: Coordinating Distributed Transactions",[15,5400,5401],{},"The Saga pattern manages long-running transactions across multiple services in a distributed system. When a single database transaction can't span service boundaries, a saga breaks the transaction into a sequence of local transactions, each publishing an event or message that triggers the next step.",[15,5403,5404],{},"There are two saga implementations:",[15,5406,5407,5410],{},[124,5408,5409],{},"Choreography-based Saga:"," Each service reacts to events and publishes events to trigger the next step. No central coordinator.",[56,5412,5415],{"className":5413,"code":5414,"language":3784},[3782],"OrderService publishes OrderCreated\n→ InventoryService reacts, reserves stock, publishes InventoryReserved\n→ PaymentService reacts, charges customer, publishes PaymentProcessed\n→ OrderService reacts, marks order as confirmed\n",[33,5416,5414],{"__ignoreMap":61},[15,5418,5419,5420,5423],{},"Failure handling requires compensating transactions: if payment fails after inventory is reserved, publish ",[33,5421,5422],{},"InventoryReservationCancelled"," to trigger the release.",[15,5425,5426,5429],{},[124,5427,5428],{},"Orchestration-based Saga:"," A central orchestrator sends commands to each service and handles responses.",[56,5431,5434],{"className":5432,"code":5433,"language":3784},[3782],"OrderSaga sends ReserveInventory to InventoryService\n→ InventoryService responds with InventoryReserved\n→ OrderSaga sends ChargeCustomer to PaymentService\n→ PaymentService responds with PaymentFailed\n→ OrderSaga sends ReleaseInventory to InventoryService (compensating transaction)\n",[33,5435,5433],{"__ignoreMap":61},[15,5437,5438],{},"Orchestration is easier to understand and debug but creates a central coordination dependency. Choreography is more resilient but harder to trace. Choose based on how complex the failure handling is and how important visibility into saga state is.",[15,5440,5441],{},"The Saga pattern is essential when you have multi-step business processes that span services and need to handle partial failures gracefully.",[746,5443],{},[22,5445,5447],{"id":5446},"outbox-pattern-reliable-event-publishing","Outbox Pattern: Reliable Event Publishing",[15,5449,5450],{},"The Outbox pattern solves a specific but critical problem: how do you atomically update a database and publish an event to a message broker?",[15,5452,5453],{},"If you write to the database and then publish to Kafka, what happens when the broker is unavailable? The database write succeeds but the event is never published. Downstream consumers miss the event. State diverges.",[15,5455,5456,5457,5460],{},"The Outbox pattern writes the event to an ",[33,5458,5459],{},"outbox"," table in the same database transaction as the state change:",[56,5462,5464],{"className":58,"code":5463,"language":60,"meta":61,"style":61},"BEGIN TRANSACTION;\n UPDATE orders SET status = 'confirmed' WHERE id = $1;\n INSERT INTO outbox (event_type, payload)\n VALUES ('OrderConfirmed', '{\"orderId\": \"...\"}');\nCOMMIT;\n",[33,5465,5466,5471,5476,5481,5486],{"__ignoreMap":61},[65,5467,5468],{"class":67,"line":68},[65,5469,5470],{},"BEGIN TRANSACTION;\n",[65,5472,5473],{"class":67,"line":74},[65,5474,5475],{}," UPDATE orders SET status = 'confirmed' WHERE id = $1;\n",[65,5477,5478],{"class":67,"line":80},[65,5479,5480],{}," INSERT INTO outbox (event_type, payload)\n",[65,5482,5483],{"class":67,"line":86},[65,5484,5485],{}," VALUES ('OrderConfirmed', '{\"orderId\": \"...\"}');\n",[65,5487,5488],{"class":67,"line":92},[65,5489,2578],{},[15,5491,5492,5493,5495],{},"A separate process (the outbox relay) polls the ",[33,5494,5459],{}," table, publishes the events to the message broker, and marks them as published. The database transaction guarantees that the state change and the outbox entry either both succeed or both fail — the event publication is eventually guaranteed.",[15,5497,5498],{},"This pattern is foundational for event-driven microservices that need to reliably publish events without distributed transaction coordination.",[746,5500],{},[22,5502,5504],{"id":5503},"combining-patterns-at-the-architectural-level","Combining Patterns at the Architectural Level",[15,5506,5507],{},"The real power of these patterns emerges when they're composed. A typical order processing flow might combine:",[118,5509,5510,5516,5522,5528,5534,5540],{},[121,5511,5512,5515],{},[124,5513,5514],{},"Repository"," to abstract data access",[121,5517,5518,5521],{},[124,5519,5520],{},"Factory"," to instantiate the right payment strategy",[121,5523,5524,5527],{},[124,5525,5526],{},"Strategy"," to execute the payment through the appropriate provider",[121,5529,5530,5533],{},[124,5531,5532],{},"Observer"," (domain events) to decouple order confirmation from downstream effects",[121,5535,5536,5539],{},[124,5537,5538],{},"Outbox"," to reliably publish domain events to the message broker",[121,5541,5542,5545],{},[124,5543,5544],{},"Saga"," to coordinate the multi-service fulfillment flow",[15,5547,5548],{},"Each pattern addresses a specific structural concern. Together they produce a system where business logic is clear and isolated, infrastructure is pluggable, and distributed workflows are managed reliably.",[746,5550],{},[15,5552,5553],{},"Patterns are not solutions you reach for to signal sophistication. They're tools you reach for when the problem they solve is the problem you have. The architect's job is to recognize when a pattern fits, apply it at the right level, and have the judgment to leave it out when it doesn't add value.",[746,5555],{},[15,5557,5558,5559],{},"If you're designing a system and want to think through which patterns apply to your specific architectural challenges, ",[752,5560,5562],{"href":754,"rel":5561},[756],"let's have that conversation.",[746,5564],{},[22,5566,764],{"id":763},[118,5568,5569,5575,5581,5587],{},[121,5570,5571],{},[752,5572,5574],{"href":5573},"/blog/software-architecture-patterns","Software Architecture Patterns Every Architect Should Know",[121,5576,5577],{},[752,5578,5580],{"href":5579},"/blog/software-architect-vs-software-engineer","Software Architect vs Software Engineer: What's Actually Different",[121,5582,5583],{},[752,5584,5586],{"href":5585},"/blog/what-is-a-software-architect","What Is a Software Architect? (And Why Your Business Needs One)",[121,5588,5589],{},[752,5590,5592],{"href":5591},"/blog/distributed-systems-fundamentals","Distributed Systems Fundamentals Every Developer Should Know",[792,5594,5595],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":61,"searchDepth":80,"depth":80,"links":5597},[5598,5599,5600,5601,5602,5603,5604,5605,5606],{"id":4547,"depth":74,"text":4548},{"id":4562,"depth":74,"text":4563},{"id":4788,"depth":74,"text":4789},{"id":4957,"depth":74,"text":4958},{"id":5218,"depth":74,"text":5219},{"id":5397,"depth":74,"text":5398},{"id":5446,"depth":74,"text":5447},{"id":5503,"depth":74,"text":5504},{"id":763,"depth":74,"text":764},"Architecture","Software design patterns become architectural tools when applied at the right scale. Here's how Factory, Strategy, Observer, Saga, Outbox, and Repository patterns serve architectural goals beyond their textbook definitions.",[5610,5611,5612,5613,5614],"software design patterns","architectural design patterns","saga pattern","outbox pattern","repository pattern",{},"/blog/design-patterns-for-architects",{"title":4541,"description":5608},"blog/design-patterns-for-architects",[5620,5621,5622,5623],"Design Patterns","Software Architecture","Systems Design","Software Engineering","gBrPCDMhWjaRkrHyBZILdf1sVRpf0i2TX0i2WkMrUVc",{"id":5626,"title":5627,"author":5628,"body":5629,"category":5607,"date":809,"description":5934,"extension":811,"featured":812,"image":813,"keywords":5935,"meta":5941,"navigation":278,"path":5942,"readTime":367,"seo":5943,"stem":5944,"tags":5945,"__hash__":5950},"blog/blog/developer-experience-improvements.md","Developer Experience: The Hidden Multiplier on Team Output",{"name":9,"bio":10},{"type":12,"value":5630,"toc":5917},[5631,5635,5638,5641,5644,5646,5650,5653,5676,5679,5681,5685,5688,5691,5695,5709,5718,5724,5730,5732,5736,5739,5753,5757,5763,5769,5775,5781,5787,5789,5793,5796,5800,5803,5806,5810,5816,5822,5828,5830,5834,5837,5840,5850,5856,5862,5864,5868,5871,5874,5877,5880,5882,5889,5891,5893],[22,5632,5634],{"id":5633},"the-compound-effect-nobody-talks-about","The Compound Effect Nobody Talks About",[15,5636,5637],{},"If your CI pipeline takes 20 minutes instead of 5 minutes, every engineer on your team loses 15 minutes per deploy cycle. Across a 10-person team running 5 deploys per day, that's 12.5 engineer-hours per day wasted. Over a month, that's roughly 250 hours — the equivalent of 6 full engineering weeks, paid for in productivity loss while the CI timer ticks.",[15,5639,5640],{},"That calculation doesn't account for context switching. When a developer pushes code and has to wait 20 minutes for feedback, they don't sit idle — they switch to another task. Context switching has a restoration cost: it takes time to get back into the mental model of the original task. A slow feedback loop doesn't just cost the wait time. It costs the re-entry time as well.",[15,5642,5643],{},"This is what makes developer experience (DX) a multiplier rather than a nice-to-have. Every friction point in the development workflow compounds across every engineer on your team, every day. Investments in DX don't improve one team member's productivity — they improve everyone's, simultaneously, for as long as the improvement exists.",[746,5645],{},[22,5647,5649],{"id":5648},"what-developer-experience-actually-means","What Developer Experience Actually Means",[15,5651,5652],{},"Developer experience is the sum of all the friction a developer encounters while building, testing, deploying, and operating software. It includes:",[118,5654,5655,5658,5661,5664,5667,5670,5673],{},[121,5656,5657],{},"How long it takes to get the local development environment running from scratch",[121,5659,5660],{},"How quickly tests run and whether the test suite is trustworthy",[121,5662,5663],{},"How long the CI/CD pipeline takes from push to deployment",[121,5665,5666],{},"How easy it is to find information about how the system works",[121,5668,5669],{},"How clear the deployment process is and how often it fails",[121,5671,5672],{},"How fast the IDE responds and how good the tooling is",[121,5674,5675],{},"How much cognitive overhead the development workflow imposes",[15,5677,5678],{},"Good DX doesn't mean every tool is perfectly configured — it means the aggregate friction is low enough that engineers spend the majority of their time on the actual problem rather than on the tooling around it.",[746,5680],{},[22,5682,5684],{"id":5683},"local-development-the-first-30-minutes-test","Local Development: The First 30 Minutes Test",[15,5686,5687],{},"The single best diagnostic for your team's developer experience: have an experienced engineer new to the team try to run your system locally from nothing, and time how long it takes.",[15,5689,5690],{},"If the answer is more than 30 minutes — including time reading the README, installing dependencies, configuring environment variables, and running the first successful test — you have a DX problem. If the answer involves Slack messages asking teammates for help, you have a documentation problem and a DX problem.",[938,5692,5694],{"id":5693},"what-good-local-dev-setup-looks-like","What Good Local Dev Setup Looks Like",[15,5696,5697,5700,5701,5704,5705,5708],{},[124,5698,5699],{},"A single command bootstraps the environment."," Whether it's ",[33,5702,5703],{},"docker-compose up",", a ",[33,5706,5707],{},"make dev"," target, or a dev container configuration, the first command should produce a running system. Dependencies, databases, seed data — all of it comes up without manual steps.",[15,5710,5711,2356,5714,5717],{},[124,5712,5713],{},"Environment variables have documented defaults.",[33,5715,5716],{},".env.example"," is committed to the repository with every variable listed, safe defaults filled in where possible, and a comment on each variable explaining what it does and where to get the value.",[15,5719,5720,5723],{},[124,5721,5722],{},"The README is accurate."," This requires that setup instructions be tested on clean machines periodically. Instructions that worked when someone wrote them three months ago may not work after dependency updates or configuration changes.",[15,5725,5726,5729],{},[124,5727,5728],{},"Fast feedback loops."," Hot reload for the server and UI means code changes are visible in seconds, not minutes. This is especially important for UI work where tight visual feedback loops dramatically accelerate iteration.",[746,5731],{},[22,5733,5735],{"id":5734},"cicd-the-productivity-tax","CI/CD: The Productivity Tax",[15,5737,5738],{},"CI pipeline speed has an outsized effect on team velocity because the feedback loop from push to confidence affects every commit. Long pipelines have four compounding costs:",[2396,5740,5741,5744,5747,5750],{},[121,5742,5743],{},"Engineers wait longer to learn if their change broke something",[121,5745,5746],{},"Slow pipelines incentivize infrequent commits and large batch sizes",[121,5748,5749],{},"Long-running builds are expensive in compute cost",[121,5751,5752],{},"Pipeline failures that take 25 minutes to reproduce are hard to diagnose and fix",[938,5754,5756],{"id":5755},"getting-ci-under-10-minutes","Getting CI Under 10 Minutes",[15,5758,5759,5762],{},[124,5760,5761],{},"Parallelize aggressively."," Run unit tests, linting, type checking, and build steps in parallel rather than sequentially. Most test suites can be parallelized across multiple workers.",[15,5764,5765,5768],{},[124,5766,5767],{},"Cache dependencies."," Npm install and Pip install shouldn't run from scratch on every CI run. Cache node_modules, virtual environments, and other resolved dependency directories between runs. This alone often cuts 3-5 minutes from pipelines.",[15,5770,5771,5774],{},[124,5772,5773],{},"Separate fast gates from slow ones."," Unit tests should provide feedback in under 2 minutes. Integration tests and E2E tests are slower — run them after fast gates pass, or on a separate schedule (before merge vs on push). Developers should get fast feedback on the most common failures without waiting for the full suite.",[15,5776,5777,5780],{},[124,5778,5779],{},"Prune your test suite."," Slow tests are often either doing too much (integration tests masquerading as unit tests) or testing trivial behavior. Audit your test suite for tests that run slowly without providing proportionate confidence.",[15,5782,5783,5786],{},[124,5784,5785],{},"Profile before optimizing."," Most CI slowdowns have one or two dominant causes. Measure before you optimize so you know where the time is actually going.",[746,5788],{},[22,5790,5792],{"id":5791},"tooling-the-signal-to-noise-problem","Tooling: The Signal-to-Noise Problem",[15,5794,5795],{},"Developer tooling choices determine how much cognitive overhead the development workflow imposes. The goal isn't the most powerful tools — it's the right tools, well configured, with consistent team-wide adoption.",[938,5797,5799],{"id":5798},"ide-and-editor-configuration","IDE and Editor Configuration",[15,5801,5802],{},"Team-consistent code formatting eliminates an entire class of review friction. ESLint, Prettier, and editor config files committed to the repository mean every engineer's editor formats code the same way. No more \"changed whitespace\" diffs. No more style debates in code reviews.",[15,5804,5805],{},"Type checking is DX. TypeScript, or type annotations in Python, surfaces errors at development time rather than runtime. The cost is upfront configuration; the benefit is fewer runtime surprises and better IDE completeness.",[938,5807,5809],{"id":5808},"local-development-tooling","Local Development Tooling",[15,5811,5812,5815],{},[124,5813,5814],{},"Docker Compose for dependencies."," Running Postgres, Redis, and Kafka locally through Docker Compose eliminates \"it works on my machine\" inconsistencies. Services behave the same on every developer's machine because they're running the same containers.",[15,5817,5818,5821],{},[124,5819,5820],{},"Database seeding."," New engineers shouldn't have to create test data manually. A seed script that creates a consistent, representative dataset means everyone has the same starting point.",[15,5823,5824,5827],{},[124,5825,5826],{},"Scripts for common tasks."," If engineers run the same sequence of commands regularly — reset the database, rebuild migrations, clear caches — those commands should be in a Makefile or script. Documented, versioned, consistent.",[746,5829],{},[22,5831,5833],{"id":5832},"observability-as-a-dx-tool","Observability as a DX Tool",[15,5835,5836],{},"Developer experience extends into production operations. If engineers can't easily understand what's happening in their service — why a request is slow, what error a user encountered, which service is the bottleneck — debugging time explodes.",[15,5838,5839],{},"Good observability for developer experience means:",[15,5841,5842,5845,5846,5849],{},[124,5843,5844],{},"Structured logging."," JSON logs with consistent fields (request ID, user ID, service name, duration) that can be searched and filtered. ",[33,5847,5848],{},"console.log(\"error occurred\")"," is not observability.",[15,5851,5852,5855],{},[124,5853,5854],{},"Distributed traces."," In a microservices environment, being able to follow a single request across five services — seeing exactly where the latency is and which service returned an error — is transformative. Without it, debugging cross-service issues requires coordination and guesswork.",[15,5857,5858,5861],{},[124,5859,5860],{},"Local observability."," Developers should be able to access logs and traces in their local environment, not just production. Running Jaeger or Grafana Tempo locally alongside the application services means developers can verify observability instrumentation and trace complex scenarios before they deploy.",[746,5863],{},[22,5865,5867],{"id":5866},"dx-as-a-competitive-advantage","DX as a Competitive Advantage",[15,5869,5870],{},"Organizations with excellent developer experience ship faster, attract better engineers, and retain them longer. The economics are compelling.",[15,5872,5873],{},"An engineer spending 2 extra hours per day on DX friction over a year is 500 hours of lost productivity per engineer. Across a 20-person team, that's 10,000 hours — equivalent to roughly 5 full-time engineers doing nothing productive. Investing 1,000 engineer-hours in DX improvements to recover even half of that friction pays for itself in months.",[15,5875,5876],{},"More importantly: engineers are acutely sensitive to the quality of their working environment. The best engineers have options. They will leave environments with terrible tooling, slow feedback loops, and disorganized workflows for environments where they can do good work. Your developer experience is part of your talent offer.",[15,5878,5879],{},"Build your tooling with the same care you build your product.",[746,5881],{},[15,5883,5884,5885],{},"If you're looking to systematically improve developer experience on an engineering team — whether that's CI speed, local dev setup, or observability — ",[752,5886,5888],{"href":754,"rel":5887},[756],"let's connect.",[746,5890],{},[22,5892,764],{"id":763},[118,5894,5895,5901,5907,5913],{},[121,5896,5897],{},[752,5898,5900],{"href":5899},"/blog/platform-engineering-explained","Platform Engineering Explained (And Why It's Not Just DevOps)",[121,5902,5903],{},[752,5904,5906],{"href":5905},"/blog/software-documentation-best-practices","Software Documentation That Engineers Actually Read",[121,5908,5909],{},[752,5910,5912],{"href":5911},"/blog/architecture-decision-records","Architecture Decision Records: Why You Need Them and How to Write Them",[121,5914,5915],{},[752,5916,5592],{"href":5591},{"title":61,"searchDepth":80,"depth":80,"links":5918},[5919,5920,5921,5924,5927,5931,5932,5933],{"id":5633,"depth":74,"text":5634},{"id":5648,"depth":74,"text":5649},{"id":5683,"depth":74,"text":5684,"children":5922},[5923],{"id":5693,"depth":80,"text":5694},{"id":5734,"depth":74,"text":5735,"children":5925},[5926],{"id":5755,"depth":80,"text":5756},{"id":5791,"depth":74,"text":5792,"children":5928},[5929,5930],{"id":5798,"depth":80,"text":5799},{"id":5808,"depth":80,"text":5809},{"id":5832,"depth":74,"text":5833},{"id":5866,"depth":74,"text":5867},{"id":763,"depth":74,"text":764},"Developer experience improvements compound directly into engineering productivity. Here's what actually moves the needle — from local dev setup to CI speed — and why DX is a competitive advantage.",[5936,5937,5938,5939,5940],"developer experience","developer experience improvements","DX engineering","engineering productivity","developer tooling",{},"/blog/developer-experience-improvements",{"title":5627,"description":5934},"blog/developer-experience-improvements",[5946,5947,5948,5949],"Developer Experience","Platform Engineering","Engineering Culture","Productivity","rNXr8bNwRdiH4qwCZIEssu0vH4ajjj_eqxnuHsy8WB0",{"id":5952,"title":5953,"author":5954,"body":5955,"category":6183,"date":809,"description":6184,"extension":811,"featured":812,"image":813,"keywords":6185,"meta":6188,"navigation":278,"path":6189,"readTime":104,"seo":6190,"stem":6191,"tags":6192,"__hash__":6196},"blog/blog/developer-productivity-tools.md","Developer Productivity: The Tools and Habits That Actually Move the Needle",{"name":9,"bio":10},{"type":12,"value":5956,"toc":6173},[5957,5961,5964,5967,5970,5972,5976,5979,5982,5988,5994,6000,6002,6006,6009,6016,6019,6021,6025,6028,6034,6040,6046,6049,6051,6055,6058,6061,6064,6070,6076,6082,6088,6090,6094,6097,6100,6103,6117,6120,6122,6126,6129,6132,6135,6137,6143,6145,6147],[22,5958,5960],{"id":5959},"productivity-is-not-about-working-more-hours","Productivity Is Not About Working More Hours",[15,5962,5963],{},"The developer productivity conversation online tends toward a few recurring tropes: wake up earlier, use a Pomodoro timer, install this plugin, follow this morning routine. Most of this is noise that optimizes around the edges of the actual problem.",[15,5965,5966],{},"Real developer productivity is about reducing friction — the time and mental energy spent on things that aren't the actual problem you're trying to solve. The friction comes from slow tooling, context switching, unclear requirements, poor local environment setup, and bad habits that compound against you over time. Address the actual friction sources and the productivity follows.",[15,5968,5969],{},"Here's what I've found actually moves the needle, accumulated over years of building production systems.",[746,5971],{},[22,5973,5975],{"id":5974},"the-editor-is-not-the-problem-but-it-matters","The Editor Is Not the Problem (But It Matters)",[15,5977,5978],{},"VS Code is dominant for a reason — the extension ecosystem is enormous, the debugger integration is excellent, and the Copilot integration (or Claude integration, depending on your preference) works well in the flow of actual coding. I'm not going to tell you which editor to use.",[15,5980,5981],{},"What I will tell you is that your editor configuration matters more than which editor you pick. Specifically:",[15,5983,5984,5987],{},[124,5985,5986],{},"Language server protocol (LSP) setup."," If your editor isn't giving you jump-to-definition, auto-imports, inline type errors, and symbol renaming for every language you write in, you're wasting time. Getting LSP fully configured for TypeScript, Go, Python, or whatever your stack requires is a one-time investment that saves you minutes every day.",[15,5989,5990,5993],{},[124,5991,5992],{},"A keyboard-driven workflow."," The mouse is slow. Learning the keyboard shortcuts for your most-used operations — find in project, rename symbol, go to file, run test under cursor — compounds quickly. Track which operations you're doing with the mouse and replace them one at a time.",[15,5995,5996,5999],{},[124,5997,5998],{},"A terminal integrated into the editor."," Switching between your editor and a separate terminal window is a context switch. Integrated terminals eliminate it.",[746,6001],{},[22,6003,6005],{"id":6004},"local-development-environment-quality","Local Development Environment Quality",[15,6007,6008],{},"Bad local dev environments are a massive, underrated productivity drain. If your project takes 3 minutes to start, every restart costs you focus. If the hot-reload doesn't work reliably, you develop a habit of manually refreshing and second-guessing whether your change took effect. If your local database doesn't match production schema, you spend time debugging environment differences.",[15,6010,6011,6012,6015],{},"Invest in Docker Compose for local service management. Write the compose file once — database, cache, message queue, whatever your stack needs — and ",[33,6013,6014],{},"docker compose up"," becomes a reliable, reproducible environment for every developer on the team.",[15,6017,6018],{},"For TypeScript and Node.js, TSX or similar watch-mode runners have made \"restart to see changes\" mostly obsolete. Eliminate that friction once and stop thinking about it forever.",[746,6020],{},[22,6022,6024],{"id":6023},"ai-pair-programming-what-actually-helps","AI Pair Programming: What Actually Helps",[15,6026,6027],{},"I use AI assistance in my development workflow daily, and I've developed a clear model of where it helps and where it doesn't.",[15,6029,6030,6033],{},[124,6031,6032],{},"High value:"," Boilerplate generation for patterns you know well, explaining unfamiliar APIs or libraries, converting between data structures, writing test cases for functions you've already written, catching typos and logic errors on review.",[15,6035,6036,6039],{},[124,6037,6038],{},"Moderate value:"," First drafts of new components or modules in familiar patterns, generating SQL queries for known schemas, writing documentation.",[15,6041,6042,6045],{},[124,6043,6044],{},"Low value:"," Complex architectural decisions, novel business logic in unfamiliar domains, anything where the specification isn't clear enough that you'd know immediately if the output was wrong.",[15,6047,6048],{},"The failure mode with AI tooling is accepting output you haven't understood. Every line of generated code is your responsibility to review and comprehend before it goes into production. Developers who use AI as a way to ship code they don't understand end up with codebases they can't maintain.",[746,6050],{},[22,6052,6054],{"id":6053},"the-context-switching-problem","The Context Switching Problem",[15,6056,6057],{},"Context switching is the biggest productivity killer on most development teams, and it's almost entirely a scheduling and communication problem rather than a tooling problem.",[15,6059,6060],{},"The research on this is well-established: it takes roughly 20 minutes to get back to full focus after an interruption. If you're getting interrupted (Slack messages, meeting invites, quick questions) three times in the morning, you're losing an hour of deep work capacity before lunch.",[15,6062,6063],{},"Strategies that work:",[15,6065,6066,6069],{},[124,6067,6068],{},"Protected work blocks."," Two-hour minimum blocks where you're not available for anything non-emergency. Communicate this to your team. Turn off Slack notifications. The work that matters gets done in these blocks.",[15,6071,6072,6075],{},[124,6073,6074],{},"Async communication as default."," If a question can wait an hour, it should go in Slack rather than a tap on the shoulder. Normalize the expectation that responses aren't immediate. The exception is genuine blockers — someone can't proceed without an answer. The rule is everything else can wait.",[15,6077,6078,6081],{},[124,6079,6080],{},"Batching meetings."," If you can schedule your meetings in a block (e.g., all Tuesday and Thursday afternoons) rather than scattered throughout the week, your remaining days become deep work days. This is a scheduling discipline that requires buy-in from your team but is transformative when it works.",[15,6083,6084,6087],{},[124,6085,6086],{},"Work in progress limits."," The most productive developers I know don't context-switch between five tasks — they have one or two active tasks and don't start new ones until something is done. This is a personal Kanban principle, and it applies whether or not your team uses a formal board.",[746,6089],{},[22,6091,6093],{"id":6092},"documentation-as-a-productivity-investment","Documentation as a Productivity Investment",[15,6095,6096],{},"Most developers treat documentation as overhead — something you do at the end, reluctantly, because someone will complain if you don't. This is exactly backwards.",[15,6098,6099],{},"Good documentation is a productivity investment with a deferred return. The 30 minutes you spend writing a clear README or an architecture decision record today saves you 30 minutes of re-contextualizing every time you come back to this code in three months. It also saves every other developer on your team the same 30 minutes. On a team of five, that's 2.5 hours saved per revisit.",[15,6101,6102],{},"The documentation that pays the most:",[118,6104,6105,6108,6111,6114],{},[121,6106,6107],{},"Architecture decisions and the reasoning behind them",[121,6109,6110],{},"Non-obvious environment setup steps",[121,6112,6113],{},"The \"why\" behind technical choices that look weird",[121,6115,6116],{},"Known limitations and workarounds",[15,6118,6119],{},"The documentation that pays the least: auto-generated API docs for obvious functions, outdated READMEs nobody maintains, and long design documents that nobody reads after the project starts.",[746,6121],{},[22,6123,6125],{"id":6124},"physical-setup-and-cognitive-hygiene","Physical Setup and Cognitive Hygiene",[15,6127,6128],{},"This is the category where the productivity advice is actually right, but for boring reasons: cognitive performance is a physical phenomenon. Sleep, exercise, and a decent ergonomic setup affect code quality in ways that no tooling investment can compensate for.",[15,6130,6131],{},"Specifically: a standing desk or a good chair prevents the physical discomfort that accumulates into irritability and poor focus after 3 PM. A second monitor reduces context switching for tasks that involve referencing one thing while writing another. A good display reduces eye strain that causes you to stop working earlier than you'd like.",[15,6133,6134],{},"None of this is exciting. But the developer who consistently works well in a healthy physical setup outperforms the developer on the $700 gaming chair who's exhausted by 2 PM.",[746,6136],{},[15,6138,6139,6140,758],{},"The tools and habits above aren't hacks. They're deliberate investments in the conditions that allow focused, high-quality work. If you're building a development practice and want to think through your setup, book a conversation at ",[752,6141,757],{"href":754,"rel":6142},[756],[746,6144],{},[22,6146,764],{"id":763},[118,6148,6149,6155,6161,6167],{},[121,6150,6151],{},[752,6152,6154],{"href":6153},"/blog/building-a-developer-portfolio","Building a Developer Portfolio That Converts: Beyond the GitHub Link",[121,6156,6157],{},[752,6158,6160],{"href":6159},"/blog/how-to-become-it-project-manager","How to Become an IT Project Manager (From Developer to Project Lead)",[121,6162,6163],{},[752,6164,6166],{"href":6165},"/blog/it-project-manager-certification","IT Project Manager Certifications: Which Ones Actually Matter",[121,6168,6169],{},[752,6170,6172],{"href":6171},"/blog/technical-interview-guide","Technical Interviews: What They're Actually Testing (And How to Prepare)",{"title":61,"searchDepth":80,"depth":80,"links":6174},[6175,6176,6177,6178,6179,6180,6181,6182],{"id":5959,"depth":74,"text":5960},{"id":5974,"depth":74,"text":5975},{"id":6004,"depth":74,"text":6005},{"id":6023,"depth":74,"text":6024},{"id":6053,"depth":74,"text":6054},{"id":6092,"depth":74,"text":6093},{"id":6124,"depth":74,"text":6125},{"id":763,"depth":74,"text":764},"Career","Developer productivity advice is full of noise. Here's what I've found actually matters — the tools, habits, and environment decisions that compound over time.",[6186,6187],"developer productivity","developer tools",{},"/blog/developer-productivity-tools",{"title":5953,"description":6184},"blog/developer-productivity-tools",[6193,6194,6195],"Developer Productivity","Tools","Career Growth","HxqhBr6nL9Wom8k4sWboH46DW6lfVDSdEsKo0FY0zew",{"id":6198,"title":6199,"author":6200,"body":6201,"category":808,"date":809,"description":6448,"extension":811,"featured":812,"image":813,"keywords":6449,"meta":6452,"navigation":278,"path":6453,"readTime":431,"seo":6454,"stem":6455,"tags":6456,"__hash__":6461},"blog/blog/digital-transformation-guide.md","Digital Transformation That Sticks (Not the Buzzword Version)",{"name":9,"bio":10},{"type":12,"value":6202,"toc":6438},[6203,6207,6210,6213,6216,6219,6223,6226,6229,6232,6243,6246,6249,6253,6256,6259,6262,6265,6268,6271,6275,6278,6281,6284,6290,6296,6302,6308,6314,6318,6321,6324,6327,6333,6339,6345,6351,6355,6358,6361,6364,6370,6376,6382,6386,6389,6392,6395,6398,6401,6408,6410,6412],[22,6204,6206],{"id":6205},"the-word-stopped-meaning-anything","The Word Stopped Meaning Anything",[15,6208,6209],{},"\"Digital transformation\" became a business buzzword somewhere around 2015 and has been getting progressively more meaningless ever since. Every consulting deck includes it. Every vendor promises to deliver it. Every CIO has a transformation initiative underway.",[15,6211,6212],{},"What most of it actually involves: buying new software, running a PowerPoint presentation about the future, and then watching organizational behavior change very little while the new software gets used in ways that recreate the problems it was supposed to solve.",[15,6214,6215],{},"I've been involved in enough of these initiatives — successful ones and failed ones — to have a clear view of what separates them. The difference is not technology. It's depth of commitment to changing how work actually gets done.",[15,6217,6218],{},"Here's what real digital transformation looks like.",[22,6220,6222],{"id":6221},"start-with-the-problem-not-the-technology","Start With the Problem, Not the Technology",[15,6224,6225],{},"The first failure mode is the most common: deciding to adopt a technology before defining the problem it's supposed to solve.",[15,6227,6228],{},"\"We need to digital transform our operations\" is not a problem statement. Neither is \"we need to be more data-driven\" or \"we need to modernize our systems.\" These are directions, not destinations.",[15,6230,6231],{},"A real problem statement is specific and connected to business outcomes:",[118,6233,6234,6237,6240],{},[121,6235,6236],{},"\"We are losing 15% of leads because our sales team has no visibility into prospect history and interactions take too long to document.\"",[121,6238,6239],{},"\"Our month-end close takes 18 days because we're manually reconciling three systems that should have the same numbers.\"",[121,6241,6242],{},"\"We are filling 60% of customer service calls with status inquiries that should be self-service.\"",[15,6244,6245],{},"These are solvable problems. You can design a solution, measure whether it worked, and connect it to business value. \"Digital transformation\" as a goal cannot be measured, cannot be evaluated, and therefore cannot succeed.",[15,6247,6248],{},"Before any technology decision, document the specific, measurable problems you're solving and the outcomes that would constitute success.",[22,6250,6252],{"id":6251},"process-first-technology-second","Process First, Technology Second",[15,6254,6255],{},"Here's the principle most transformation initiatives violate: technology amplifies your existing process. If your process is broken, technology makes it more efficiently broken.",[15,6257,6258],{},"I've watched companies implement Salesforce into a sales team with no defined sales process. The system gets configured around the ad-hoc, inconsistent way individual reps work. Six months later, data quality is terrible because reps are logging activities in inconsistent ways, pipeline forecasting is unreliable, and the sales manager is making the same gut-feel decisions they made before — just with a more expensive tool.",[15,6260,6261],{},"The Salesforce didn't fail. The process failure was imported into the system and amplified.",[15,6263,6264],{},"Real transformation requires documenting your current-state process, identifying what's actually broken about it, designing an improved future-state process, and then choosing technology that supports and enforces the improved process. Not the reverse.",[15,6266,6267],{},"This order matters. Process design is harder than technology selection. It requires honest conversations about what isn't working and why. It surfaces organizational conflicts and unclear ownership. It takes longer than a demo. Most organizations skip it because it's uncomfortable and because vendors make technology selection feel more productive.",[15,6269,6270],{},"Process work that isn't done before implementation gets done after, under pressure, with degraded results.",[22,6272,6274],{"id":6273},"change-management-is-half-the-project","Change Management Is Half the Project",[15,6276,6277],{},"The technical implementation of most digital transformation initiatives is the easier half. The change management — getting people to actually use the new system, in the new way, consistently — is where most projects fail.",[15,6279,6280],{},"People resist change for understandable reasons. The new system is unfamiliar. The old way worked well enough. Nobody consulted them about the new way. They're being asked to change their workflow in the middle of a busy quarter. Their concerns weren't heard in the design phase.",[15,6282,6283],{},"Change management done well is not a series of communication emails. It's:",[15,6285,6286,6289],{},[124,6287,6288],{},"Early involvement of affected users."," The people who will use the system every day should be part of the design process, not recipients of the completed design. They know things about the actual workflow that the project team doesn't. Involving them creates ownership and surfaces problems before they're baked into configuration.",[15,6291,6292,6295],{},[124,6293,6294],{},"Honest communication about what changes and why."," People can handle change better than they can handle uncertainty and surprises. Tell them early what's changing, why, and what it means for their day-to-day work. The \"why\" matters — people don't need to agree with every decision, but they need to understand the rationale.",[15,6297,6298,6301],{},[124,6299,6300],{},"Training that prepares people for their actual job."," Generic system training is inadequate. Role-specific training, timed close to go-live, with hands-on practice in realistic scenarios, is what actually changes behavior.",[15,6303,6304,6307],{},[124,6305,6306],{},"Visible leadership support."," When the VP of Operations is the first person to enter data in the new system and visibly uses it in leadership meetings, it signals to the team that this change is real and expected of everyone. When leadership continues using the old system while telling the team to use the new one, it signals that the change is optional.",[15,6309,6310,6313],{},[124,6311,6312],{},"Sustained support through the adoption period."," The first 90 days are critical. People need quick answers to questions. Problems need fast resolution. If the support structure isn't there, people route around the new system rather than working through the friction.",[22,6315,6317],{"id":6316},"data-strategy-is-not-optional","Data Strategy Is Not Optional",[15,6319,6320],{},"Most transformation initiatives produce a new system sitting on top of the same data chaos that existed before. Inconsistent formats, missing values, duplicate records, fields that mean different things to different teams — none of this gets solved by implementing new software.",[15,6322,6323],{},"A data strategy needs to be part of the transformation initiative, not a separate future project.",[15,6325,6326],{},"Data strategy at the transformation level doesn't mean building a data warehouse (though that might be part of it). It means:",[15,6328,6329,6332],{},[124,6330,6331],{},"Defining authoritative sources."," For each important data type — customer records, product catalog, financial data — there is exactly one system of record. Other systems are allowed to read this data; they're not allowed to maintain parallel versions of it.",[15,6334,6335,6338],{},[124,6336,6337],{},"Establishing data governance."," Who can create records? What fields are required? What naming conventions apply? Who is responsible for data quality in each domain? This doesn't require a massive bureaucracy, but it does require explicit ownership.",[15,6340,6341,6344],{},[124,6342,6343],{},"Cleaning before you migrate."," If you're moving data from old systems to new, the migration is the opportunity to fix it. Deduplicate. Standardize. Remove obsolete records. This is unglamorous work, but the cost of migrating bad data into a new system and then trying to clean it while the system is live is much higher.",[15,6346,6347,6350],{},[124,6348,6349],{},"Measuring data quality."," Define what good looks like — completeness rates, duplicate counts, freshness metrics — and track them. Data quality degrades without active management. Making it visible makes it manageable.",[22,6352,6354],{"id":6353},"technology-selection-last-not-first","Technology Selection: Last, Not First",[15,6356,6357],{},"After you've defined the problem, designed the future-state process, planned your change management approach, and defined your data strategy — now you're ready to evaluate technology.",[15,6359,6360],{},"At this point, technology selection is much simpler. You have specific requirements. You can score vendors against them. You know what your integration needs are. You know what your data model should look like. You can evaluate demos against your actual use cases instead of the vendor's showcase scenarios.",[15,6362,6363],{},"The technology decisions that tend to hold up:",[15,6365,6366,6369],{},[124,6367,6368],{},"Prefer configuration over customization."," Every customization is future technical debt. Prefer systems that can be configured to meet your requirements over systems that require custom development to match your process. When you can't avoid customization, document it explicitly and factor it into TCO.",[15,6371,6372,6375],{},[124,6373,6374],{},"Prioritize integration over features."," A best-in-class system that integrates poorly with your ecosystem will cost more in integration complexity than a good-enough system with excellent APIs. Integration is usually the hardest engineering problem in enterprise software.",[15,6377,6378,6381],{},[124,6379,6380],{},"Design for evolution."," Your process will change. Your business will change. The technology should be able to change with it. Vendor lock-in that prevents evolution is a real risk to price into your decision.",[22,6383,6385],{"id":6384},"the-measurement-obligation","The Measurement Obligation",[15,6387,6388],{},"Transformation initiatives that don't measure outcomes have no feedback loop. Without feedback, you can't course-correct, can't demonstrate value, and can't learn what to do differently.",[15,6390,6391],{},"Define your metrics before you start. Measure them before go-live (baseline), at 30 days, at 90 days, at one year. Share the results — good and bad — with the organization.",[15,6393,6394],{},"The teams that do this well find that transparent measurement builds credibility even when early results are disappointing. It demonstrates that the initiative is serious, that leadership is paying attention, and that problems will be identified and addressed rather than hidden.",[15,6396,6397],{},"The teams that don't measure can never answer the question every senior leader eventually asks: \"Was this worth what we spent on it?\"",[15,6399,6400],{},"Digital transformation that sticks is boring in the best sense. It's methodical, process-oriented, and focused on measurable outcomes rather than technology novelty. It produces systems that people actually use and processes that actually improve.",[15,6402,6403,6404,758],{},"If you're planning a transformation initiative and want an honest conversation about scope, approach, and what success actually looks like, ",[752,6405,6407],{"href":754,"rel":6406},[756],"schedule time at calendly.com/jamesrossjr",[746,6409],{},[22,6411,764],{"id":763},[118,6413,6414,6420,6426,6432],{},[121,6415,6416],{},[752,6417,6419],{"href":6418},"/blog/build-vs-buy-enterprise-software","Build vs Buy Enterprise Software: A Framework for the Decision",[121,6421,6422],{},[752,6423,6425],{"href":6424},"/blog/erp-vs-crm-differences","ERP vs CRM: What's the Difference and Which Do You Actually Need?",[121,6427,6428],{},[752,6429,6431],{"href":6430},"/blog/low-code-vs-custom-development","Low-Code vs Custom Development: When Each Actually Makes Sense",[121,6433,6434],{},[752,6435,6437],{"href":6436},"/blog/saas-vs-on-premise","SaaS vs On-Premise Enterprise Software: How to Make the Right Call",{"title":61,"searchDepth":80,"depth":80,"links":6439},[6440,6441,6442,6443,6444,6445,6446,6447],{"id":6205,"depth":74,"text":6206},{"id":6221,"depth":74,"text":6222},{"id":6251,"depth":74,"text":6252},{"id":6273,"depth":74,"text":6274},{"id":6316,"depth":74,"text":6317},{"id":6353,"depth":74,"text":6354},{"id":6384,"depth":74,"text":6385},{"id":763,"depth":74,"text":764},"Most digital transformation initiatives fail because they focus on technology instead of process. Here's what real, lasting digital transformation actually requires.",[6450,6451],"digital transformation","enterprise software development",{},"/blog/digital-transformation-guide",{"title":6199,"description":6448},"blog/digital-transformation-guide",[6457,6458,5526,6459,6460],"Digital Transformation","Enterprise Software","Business Process","Change Management","rqVwxCCI8H9BYhv5FvPDxwuOH_glIfPF1GqrPcmcAVQ",{"id":6463,"title":5592,"author":6464,"body":6465,"category":5607,"date":809,"description":6784,"extension":811,"featured":812,"image":813,"keywords":6785,"meta":6791,"navigation":278,"path":5591,"readTime":431,"seo":6792,"stem":6793,"tags":6794,"__hash__":6797},"blog/blog/distributed-systems-fundamentals.md",{"name":9,"bio":10},{"type":12,"value":6466,"toc":6763},[6467,6471,6474,6477,6480,6482,6486,6489,6515,6518,6521,6523,6527,6530,6550,6557,6563,6569,6572,6576,6579,6581,6585,6588,6592,6595,6598,6602,6605,6608,6612,6615,6619,6622,6624,6628,6632,6639,6642,6646,6649,6653,6656,6658,6662,6665,6674,6684,6687,6689,6693,6699,6705,6711,6717,6723,6725,6728,6730,6737,6739,6741],[22,6468,6470],{"id":6469},"why-this-matters-beyond-distributed-systems-specialists","Why This Matters Beyond Distributed Systems Specialists",[15,6472,6473],{},"For a long time, distributed systems was a specialized discipline. Most application developers worked against a single database on a single server, and the hard problems of distributed computing were someone else's problem.",[15,6475,6476],{},"That's no longer true. Modern application development almost universally involves distributed systems: microservices communicating over the network, databases replicating across nodes, caches that may be stale, queues that guarantee at-least-once delivery. If you're building web applications today, you're building distributed systems whether you think of it that way or not.",[15,6478,6479],{},"The fundamentals aren't academic. They're the foundation for making sound decisions about databases, cache invalidation, service communication, and failure handling. Here's what every developer working in this space needs to understand.",[746,6481],{},[22,6483,6485],{"id":6484},"fallacies-of-distributed-computing","Fallacies of Distributed Computing",[15,6487,6488],{},"Before the theory, a reality check. Peter Deutsch and his colleagues at Sun Microsystems articulated eight assumptions that developers commonly make about distributed systems — all of them false:",[2396,6490,6491,6494,6497,6500,6503,6506,6509,6512],{},[121,6492,6493],{},"The network is reliable",[121,6495,6496],{},"Latency is zero",[121,6498,6499],{},"Bandwidth is infinite",[121,6501,6502],{},"The network is secure",[121,6504,6505],{},"Topology doesn't change",[121,6507,6508],{},"There is one administrator",[121,6510,6511],{},"Transport cost is zero",[121,6513,6514],{},"The network is homogeneous",[15,6516,6517],{},"Every application running in a distributed environment should be designed with the understanding that these assumptions will be violated — probably at the worst possible moment. Network packets get dropped. Services go down. Latency spikes. Replication lags.",[15,6519,6520],{},"The question isn't whether failures will happen. The question is what your system does when they do.",[746,6522],{},[22,6524,6526],{"id":6525},"cap-theorem-the-honest-explanation","CAP Theorem: The Honest Explanation",[15,6528,6529],{},"The CAP theorem states that a distributed data store can guarantee at most two of three properties simultaneously:",[118,6531,6532,6538,6544],{},[121,6533,6534,6537],{},[124,6535,6536],{},"Consistency (C):"," Every read receives the most recent write or an error. All nodes see the same data at the same time.",[121,6539,6540,6543],{},[124,6541,6542],{},"Availability (A):"," Every request receives a response — not necessarily the most up-to-date, but a response. The system remains operational.",[121,6545,6546,6549],{},[124,6547,6548],{},"Partition Tolerance (P):"," The system continues operating even when network partitions (message loss or delay between nodes) occur.",[15,6551,6552,6553,6556],{},"The critical insight: ",[124,6554,6555],{},"in any real distributed system, partition tolerance is non-negotiable."," Networks partition. You can't opt out of partition tolerance; you can only decide how your system behaves when partitions occur. So the real choice is between CP and AP.",[15,6558,6559,6562],{},[124,6560,6561],{},"CP systems"," (like HBase, Zookeeper) prioritize consistency. When a partition occurs, the system refuses to serve requests rather than risk serving inconsistent data. You get strong consistency at the cost of availability during failures.",[15,6564,6565,6568],{},[124,6566,6567],{},"AP systems"," (like DynamoDB, Cassandra in its default configuration) prioritize availability. When a partition occurs, the system continues serving requests but may serve stale data. You get availability at the cost of consistency during failures.",[15,6570,6571],{},"Neither is universally correct. Financial systems often choose CP — a bank should refuse a transaction rather than process it twice. User-facing applications often choose AP — showing a user a slightly stale product count is better than showing an error page.",[938,6573,6575],{"id":6574},"the-limitation-of-cap","The Limitation of CAP",[15,6577,6578],{},"CAP is a useful mental model but an imprecise one. It treats consistency and availability as binary properties, when in reality they're spectrums. It doesn't account for the degree of inconsistency you're willing to tolerate or the frequency of partitions in your environment. The PACELC model extends CAP by also considering the latency/consistency trade-off when the network is operating normally.",[746,6580],{},[22,6582,6584],{"id":6583},"consistency-models","Consistency Models",[15,6586,6587],{},"This is where most developers' understanding gets fuzzy, because \"consistency\" means different things in different contexts.",[938,6589,6591],{"id":6590},"strong-consistency-linearizability","Strong Consistency (Linearizability)",[15,6593,6594],{},"After a write completes, all subsequent reads will return that value. The system behaves as if there were a single copy of the data. This is what most developers intuitively expect from a database.",[15,6596,6597],{},"Strong consistency is expensive in distributed systems because every write must be coordinated across all replicas before acknowledging success. Latency increases with the number of replicas and the distance between them.",[938,6599,6601],{"id":6600},"eventual-consistency","Eventual Consistency",[15,6603,6604],{},"Writes will eventually propagate to all replicas. If you write and immediately read from a different node, you might read stale data. Given enough time without new writes, all nodes will converge to the same value.",[15,6606,6607],{},"This is the consistency model of most large-scale distributed databases. It enables high availability and low latency by allowing reads to be served from local replicas without synchronization. The trade-off is that readers may see different values depending on which replica they hit and how far replication has propagated.",[938,6609,6611],{"id":6610},"causal-consistency","Causal Consistency",[15,6613,6614],{},"If event A causes event B, then every node that sees B will also have seen A. This is stronger than eventual consistency — causally related events are ordered correctly — but weaker than strong consistency. A comment on a post will always be visible after the post itself.",[938,6616,6618],{"id":6617},"read-your-writes-consistency","Read-Your-Writes Consistency",[15,6620,6621],{},"After a client performs a write, subsequent reads by that same client will reflect that write. Other clients may still see stale data. This is a common practical target for user-facing applications — users should see the changes they made immediately, even if other users might temporarily see different data.",[746,6623],{},[22,6625,6627],{"id":6626},"failure-modes-in-distributed-systems","Failure Modes in Distributed Systems",[938,6629,6631],{"id":6630},"node-failures","Node Failures",[15,6633,6634,6635,6638],{},"Individual services crash. This is the expected and well-handled failure case: load balancers detect the unhealthy node and route traffic away. The more complex scenario is ",[124,6636,6637],{},"partial failures"," — a node that's running but degraded, responding slowly, or returning errors for some requests.",[15,6640,6641],{},"Partial failures are harder to detect and more damaging because they don't trigger the same automatic mitigation as complete failures. Circuit breakers are the standard pattern: track error rates for a downstream service and stop sending requests when the error rate exceeds a threshold, allowing the service time to recover.",[938,6643,6645],{"id":6644},"network-partitions","Network Partitions",[15,6647,6648],{},"Two parts of the system can no longer communicate with each other. Both sides are healthy individually. This is the failure mode CAP theorem is concerned with. During a partition, systems must decide: do we stop accepting writes to maintain consistency, or do we accept writes on both sides and reconcile later?",[938,6650,6652],{"id":6651},"byzantine-failures","Byzantine Failures",[15,6654,6655],{},"A node behaves arbitrarily — returning incorrect data, performing malicious actions, or behaving inconsistently. Byzantine fault tolerance is relevant in adversarial environments (blockchain, voting systems) but overkill for most application-level distributed systems. It's worth knowing the concept exists and distinguishing it from crash failures.",[746,6657],{},[22,6659,6661],{"id":6660},"partitioning-and-consistent-hashing","Partitioning and Consistent Hashing",[15,6663,6664],{},"When data needs to be distributed across multiple nodes, you need a partitioning strategy that distributes load evenly and handles node additions or removals without remapping all data.",[15,6666,6667,538,6670,6673],{},[124,6668,6669],{},"Naive hash partitioning",[33,6671,6672],{},"hash(key) % N",") assigns each key to a node. The problem: when N changes (a node is added or removed), nearly every key's assignment changes, requiring a massive redistribution.",[15,6675,6676,6679,6680,6683],{},[124,6677,6678],{},"Consistent hashing"," maps both keys and nodes onto a ring. Each key is assigned to the next node clockwise on the ring. When a node is added, only the keys previously assigned to its successor need to be moved. When a node is removed, only the keys assigned to it need redistribution. On average, adding or removing a node requires moving ",[33,6681,6682],{},"K/N"," keys rather than nearly all of them.",[15,6685,6686],{},"Consistent hashing is used in distributed caches (Memcached clusters), distributed databases (Cassandra, DynamoDB), and load balancing. Understanding it helps you reason about how your data layer handles cluster topology changes.",[746,6688],{},[22,6690,6692],{"id":6691},"practical-implications-for-application-design","Practical Implications for Application Design",[15,6694,6695,6698],{},[124,6696,6697],{},"Use idempotent operations wherever possible."," Network retries will happen. If an operation produces the same result when called multiple times, retries are safe.",[15,6700,6701,6704],{},[124,6702,6703],{},"Design for partial availability."," When a downstream service is unavailable, degrade gracefully rather than propagating failures. Return cached data, a default response, or an explicit \"service unavailable\" state — don't let the failure cascade.",[15,6706,6707,6710],{},[124,6708,6709],{},"Be explicit about consistency requirements."," For each data access in your application, ask: is eventual consistency acceptable here? If a user adds an item to their cart, do they need to see it immediately on the next page? (Almost certainly yes — use read-your-writes.) Does every user need to see the same product inventory count in real time? (Probably not — eventual consistency is fine.)",[15,6712,6713,6716],{},[124,6714,6715],{},"Handle duplicate delivery."," Message queues guarantee at-least-once delivery. Build consumers that handle receiving the same message multiple times without producing incorrect results.",[15,6718,6719,6722],{},[124,6720,6721],{},"Observe everything."," Distributed systems fail in ways that are invisible without instrumentation. Distributed tracing, structured logging, and error tracking aren't optional — they're how you find out what broke and why.",[746,6724],{},[15,6726,6727],{},"Distributed systems problems are fundamentally different from single-machine problems. The solutions involve trade-offs that don't exist in simpler environments. The developers and architects who understand these fundamentals make better decisions about databases, caching strategies, service communication, and failure handling — and build systems that hold up when the inevitable failures occur.",[746,6729],{},[15,6731,6732,6733],{},"If you're designing a distributed system and want to think through the consistency and availability trade-offs for your specific requirements, ",[752,6734,6736],{"href":754,"rel":6735},[756],"I'd be glad to help.",[746,6738],{},[22,6740,764],{"id":763},[118,6742,6743,6747,6751,6757],{},[121,6744,6745],{},[752,6746,5574],{"href":5573},[121,6748,6749],{},[752,6750,4541],{"href":5616},[121,6752,6753],{},[752,6754,6756],{"href":6755},"/blog/domain-driven-design-guide","Domain-Driven Design in Practice (Without the Theory Overload)",[121,6758,6759],{},[752,6760,6762],{"href":6761},"/blog/microservices-vs-monolith","Microservices vs Monolith: The Honest Trade-off Analysis",{"title":61,"searchDepth":80,"depth":80,"links":6764},[6765,6766,6767,6770,6776,6781,6782,6783],{"id":6469,"depth":74,"text":6470},{"id":6484,"depth":74,"text":6485},{"id":6525,"depth":74,"text":6526,"children":6768},[6769],{"id":6574,"depth":80,"text":6575},{"id":6583,"depth":74,"text":6584,"children":6771},[6772,6773,6774,6775],{"id":6590,"depth":80,"text":6591},{"id":6600,"depth":80,"text":6601},{"id":6610,"depth":80,"text":6611},{"id":6617,"depth":80,"text":6618},{"id":6626,"depth":74,"text":6627,"children":6777},[6778,6779,6780],{"id":6630,"depth":80,"text":6631},{"id":6644,"depth":80,"text":6645},{"id":6651,"depth":80,"text":6652},{"id":6660,"depth":74,"text":6661},{"id":6691,"depth":74,"text":6692},{"id":763,"depth":74,"text":764},"Distributed systems fundamentals — CAP theorem, consistency models, failure modes, and partitioning — are essential knowledge for anyone building systems that run across multiple nodes or services.",[6786,6787,6788,6789,6790],"distributed systems fundamentals","CAP theorem explained","distributed systems consistency","partition tolerance","eventual consistency",{},{"title":5592,"description":6784},"blog/distributed-systems-fundamentals",[6795,5621,5622,6796],"Distributed Systems","CAP Theorem","Ulshr3Aq7aSydrg5R8f_vsVK75diaHyoqkY_3H1Sn5A",{"id":6799,"title":6800,"author":6801,"body":6802,"category":7572,"date":809,"description":7573,"extension":811,"featured":812,"image":813,"keywords":7574,"meta":7577,"navigation":278,"path":7578,"readTime":104,"seo":7579,"stem":7580,"tags":7581,"__hash__":7585},"blog/blog/docker-for-developers-guide.md","Docker for Developers: From Zero to Production Containers",{"name":9,"bio":10},{"type":12,"value":6803,"toc":7561},[6804,6807,6810,6813,6817,6820,6823,6830,6834,6837,6895,6902,6922,6939,6943,6946,7023,7026,7030,7037,7309,7319,7329,7333,7336,7343,7349,7359,7370,7374,7380,7395,7415,7421,7427,7433,7478,7482,7485,7500,7507,7511,7514,7517,7520,7522,7528,7530,7532,7558],[3743,6805,6800],{"id":6806},"docker-for-developers-from-zero-to-production-containers",[15,6808,6809],{},"I still remember the first time a junior developer on my team said \"it works on my machine.\" We had a Node.js API that ran perfectly in development, exploded in staging, and nobody could figure out why. The culprit? A subtle difference in Node versions between the dev's MacBook and the Ubuntu staging server. That was the day I mandated Docker for every project we ship.",[15,6811,6812],{},"Docker is not DevOps magic reserved for platform teams. It is a fundamental skill for any developer who ships software to production, and if you are building anything that runs on a server, you need to understand it. Here is the practical guide I wish I had when I started.",[22,6814,6816],{"id":6815},"what-docker-actually-solves","What Docker Actually Solves",[15,6818,6819],{},"The core promise of Docker is deceptively simple: package your application with everything it needs to run, and ship that package anywhere. The container includes your runtime, your dependencies, your environment variables, and your file system configuration. The host machine only needs Docker itself.",[15,6821,6822],{},"This eliminates the \"works on my machine\" problem at the root. It also means your staging environment can be byte-for-byte identical to production. When you debug a staging issue, you know the environment is not the variable.",[15,6824,6825,6826,6829],{},"Beyond consistency, Docker makes your infrastructure declarative. Your ",[33,6827,6828],{},"Dockerfile"," is a script that documents exactly how your application is assembled. That documentation lives in your repository, gets reviewed in pull requests, and is versioned alongside your code.",[22,6831,6833],{"id":6832},"writing-your-first-dockerfile","Writing Your First Dockerfile",[15,6835,6836],{},"Start with the basics. Here is a Dockerfile for a Node.js API:",[56,6838,6842],{"className":6839,"code":6840,"language":6841,"meta":61,"style":61},"language-dockerfile shiki shiki-themes github-dark","FROM node:20-alpine\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN npm ci --only=production\n\nCOPY . .\n\nEXPOSE 3000\nCMD [\"node\", \"src/index.js\"]\n","dockerfile",[33,6843,6844,6849,6853,6858,6862,6867,6872,6876,6881,6885,6890],{"__ignoreMap":61},[65,6845,6846],{"class":67,"line":68},[65,6847,6848],{},"FROM node:20-alpine\n",[65,6850,6851],{"class":67,"line":74},[65,6852,279],{"emptyLinePlaceholder":278},[65,6854,6855],{"class":67,"line":80},[65,6856,6857],{},"WORKDIR /app\n",[65,6859,6860],{"class":67,"line":86},[65,6861,279],{"emptyLinePlaceholder":278},[65,6863,6864],{"class":67,"line":92},[65,6865,6866],{},"COPY package*.json ./\n",[65,6868,6869],{"class":67,"line":98},[65,6870,6871],{},"RUN npm ci --only=production\n",[65,6873,6874],{"class":67,"line":104},[65,6875,279],{"emptyLinePlaceholder":278},[65,6877,6878],{"class":67,"line":110},[65,6879,6880],{},"COPY . .\n",[65,6882,6883],{"class":67,"line":367},[65,6884,279],{"emptyLinePlaceholder":278},[65,6886,6887],{"class":67,"line":431},[65,6888,6889],{},"EXPOSE 3000\n",[65,6891,6892],{"class":67,"line":713},[65,6893,6894],{},"CMD [\"node\", \"src/index.js\"]\n",[15,6896,6897,6898,6901],{},"A few things worth explaining here. The ",[33,6899,6900],{},"FROM node:20-alpine"," pulls the official Node.js 20 image based on Alpine Linux. Alpine images are tiny — typically under 50MB — compared to Debian-based images that can balloon to 300MB or more. For production containers, smaller is better. Smaller images pull faster, have a smaller attack surface, and cost less to store.",[15,6903,6904,6905,3826,6908,6911,6912,6915,6916,6918,6919,6921],{},"The order of the ",[33,6906,6907],{},"COPY",[33,6909,6910],{},"RUN"," instructions matters. Docker caches each layer. By copying ",[33,6913,6914],{},"package*.json"," first and running ",[33,6917,4366],{}," before copying the rest of your source, you preserve the dependency installation cache. When you change your source code but not your dependencies, Docker reuses the cached ",[33,6920,3751],{}," layer. This makes rebuilds dramatically faster.",[15,6923,6924,6926,6927,6930,6931,6934,6935,6938],{},[33,6925,4366],{}," over ",[33,6928,6929],{},"npm install"," is intentional. ",[33,6932,6933],{},"ci"," installs exactly what is in your lockfile, fails if the lockfile is out of sync, and never modifies ",[33,6936,6937],{},"package.json",". It is deterministic and appropriate for CI and production environments.",[22,6940,6942],{"id":6941},"the-multi-stage-build-pattern","The Multi-Stage Build Pattern",[15,6944,6945],{},"Production Dockerfiles should use multi-stage builds. This pattern lets you use a heavy build image to compile your application and then copy only the compiled output into a lean runtime image.",[56,6947,6949],{"className":6839,"code":6948,"language":6841,"meta":61,"style":61},"# Build stage\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# Production stage\nFROM node:20-alpine AS production\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --only=production\nCOPY --from=builder /app/dist ./dist\nEXPOSE 3000\nCMD [\"node\", \"dist/index.js\"]\n",[33,6950,6951,6956,6961,6965,6969,6974,6978,6983,6987,6992,6997,7001,7005,7009,7014,7018],{"__ignoreMap":61},[65,6952,6953],{"class":67,"line":68},[65,6954,6955],{},"# Build stage\n",[65,6957,6958],{"class":67,"line":74},[65,6959,6960],{},"FROM node:20-alpine AS builder\n",[65,6962,6963],{"class":67,"line":80},[65,6964,6857],{},[65,6966,6967],{"class":67,"line":86},[65,6968,6866],{},[65,6970,6971],{"class":67,"line":92},[65,6972,6973],{},"RUN npm ci\n",[65,6975,6976],{"class":67,"line":98},[65,6977,6880],{},[65,6979,6980],{"class":67,"line":104},[65,6981,6982],{},"RUN npm run build\n",[65,6984,6985],{"class":67,"line":110},[65,6986,279],{"emptyLinePlaceholder":278},[65,6988,6989],{"class":67,"line":367},[65,6990,6991],{},"# Production stage\n",[65,6993,6994],{"class":67,"line":431},[65,6995,6996],{},"FROM node:20-alpine AS production\n",[65,6998,6999],{"class":67,"line":713},[65,7000,6857],{},[65,7002,7003],{"class":67,"line":1043},[65,7004,6866],{},[65,7006,7007],{"class":67,"line":1049},[65,7008,6871],{},[65,7010,7011],{"class":67,"line":1190},[65,7012,7013],{},"COPY --from=builder /app/dist ./dist\n",[65,7015,7016],{"class":67,"line":1196},[65,7017,6889],{},[65,7019,7020],{"class":67,"line":1201},[65,7021,7022],{},"CMD [\"node\", \"dist/index.js\"]\n",[15,7024,7025],{},"The final image contains no build tools, no dev dependencies, no TypeScript compiler. Just the compiled JavaScript and production dependencies. Your attack surface shrinks and your image size drops significantly.",[22,7027,7029],{"id":7028},"docker-compose-for-local-development","Docker Compose for Local Development",[15,7031,7032,7033,7036],{},"Running a single container is straightforward. Running your API, a PostgreSQL database, a Redis cache, and a background worker together is where ",[33,7034,7035],{},"docker-compose.yml"," earns its keep.",[56,7038,7040],{"className":1370,"code":7039,"language":1372,"meta":61,"style":61},"version: \"3.9\"\nservices:\n api:\n build: . Ports:\n - \"3000:3000\"\n environment:\n DATABASE_URL: postgres://postgres:password@db:5432/myapp\n REDIS_URL: redis://cache:6379\n depends_on:\n db:\n condition: service_healthy\n cache:\n condition: service_started\n\n db:\n image: postgres:16-alpine\n volumes:\n - postgres_data:/var/lib/postgresql/data\n environment:\n POSTGRES_PASSWORD: password\n POSTGRES_DB: myapp\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n interval: 5s\n timeout: 5s\n retries: 5\n\n cache:\n image: redis:7-alpine\n\nVolumes:\n postgres_data:\n",[33,7041,7042,7051,7058,7065,7077,7084,7091,7100,7110,7117,7124,7134,7141,7150,7154,7160,7170,7177,7184,7190,7200,7210,7217,7235,7245,7255,7266,7271,7278,7288,7293,7301],{"__ignoreMap":61},[65,7043,7044,7046,7048],{"class":67,"line":68},[65,7045,3947],{"class":1385},[65,7047,1407],{"class":1389},[65,7049,7050],{"class":1410},"\"3.9\"\n",[65,7052,7053,7056],{"class":67,"line":74},[65,7054,7055],{"class":1385},"services",[65,7057,1390],{"class":1389},[65,7059,7060,7063],{"class":67,"line":80},[65,7061,7062],{"class":1385}," api",[65,7064,1390],{"class":1389},[65,7066,7067,7070,7072,7075],{"class":67,"line":86},[65,7068,7069],{"class":1385}," build",[65,7071,1407],{"class":1389},[65,7073,7074],{"class":1385},". Ports",[65,7076,1390],{"class":1389},[65,7078,7079,7081],{"class":67,"line":92},[65,7080,1402],{"class":1389},[65,7082,7083],{"class":1410},"\"3000:3000\"\n",[65,7085,7086,7089],{"class":67,"line":98},[65,7087,7088],{"class":1385}," environment",[65,7090,1390],{"class":1389},[65,7092,7093,7095,7097],{"class":67,"line":104},[65,7094,1433],{"class":1385},[65,7096,1407],{"class":1389},[65,7098,7099],{"class":1410},"postgres://postgres:password@db:5432/myapp\n",[65,7101,7102,7105,7107],{"class":67,"line":110},[65,7103,7104],{"class":1385}," REDIS_URL",[65,7106,1407],{"class":1389},[65,7108,7109],{"class":1410},"redis://cache:6379\n",[65,7111,7112,7115],{"class":67,"line":367},[65,7113,7114],{"class":1385}," depends_on",[65,7116,1390],{"class":1389},[65,7118,7119,7122],{"class":67,"line":431},[65,7120,7121],{"class":1385}," db",[65,7123,1390],{"class":1389},[65,7125,7126,7129,7131],{"class":67,"line":713},[65,7127,7128],{"class":1385}," condition",[65,7130,1407],{"class":1389},[65,7132,7133],{"class":1410},"service_healthy\n",[65,7135,7136,7139],{"class":67,"line":1043},[65,7137,7138],{"class":1385}," cache",[65,7140,1390],{"class":1389},[65,7142,7143,7145,7147],{"class":67,"line":1049},[65,7144,7128],{"class":1385},[65,7146,1407],{"class":1389},[65,7148,7149],{"class":1410},"service_started\n",[65,7151,7152],{"class":67,"line":1190},[65,7153,279],{"emptyLinePlaceholder":278},[65,7155,7156,7158],{"class":67,"line":1196},[65,7157,7121],{"class":1385},[65,7159,1390],{"class":1389},[65,7161,7162,7165,7167],{"class":67,"line":1201},[65,7163,7164],{"class":1385}," image",[65,7166,1407],{"class":1389},[65,7168,7169],{"class":1410},"postgres:16-alpine\n",[65,7171,7172,7175],{"class":67,"line":1207},[65,7173,7174],{"class":1385}," volumes",[65,7176,1390],{"class":1389},[65,7178,7179,7181],{"class":67,"line":1213},[65,7180,1402],{"class":1389},[65,7182,7183],{"class":1410},"postgres_data:/var/lib/postgresql/data\n",[65,7185,7186,7188],{"class":67,"line":1219},[65,7187,7088],{"class":1385},[65,7189,1390],{"class":1389},[65,7191,7192,7195,7197],{"class":67,"line":3094},[65,7193,7194],{"class":1385}," POSTGRES_PASSWORD",[65,7196,1407],{"class":1389},[65,7198,7199],{"class":1410},"password\n",[65,7201,7202,7205,7207],{"class":67,"line":3099},[65,7203,7204],{"class":1385}," POSTGRES_DB",[65,7206,1407],{"class":1389},[65,7208,7209],{"class":1410},"myapp\n",[65,7211,7212,7215],{"class":67,"line":3104},[65,7213,7214],{"class":1385}," healthcheck",[65,7216,1390],{"class":1389},[65,7218,7219,7222,7224,7227,7229,7232],{"class":67,"line":3122},[65,7220,7221],{"class":1385}," test",[65,7223,4158],{"class":1389},[65,7225,7226],{"class":1410},"\"CMD-SHELL\"",[65,7228,2900],{"class":1389},[65,7230,7231],{"class":1410},"\"pg_isready -U postgres\"",[65,7233,7234],{"class":1389},"]\n",[65,7236,7238,7240,7242],{"class":67,"line":7237},24,[65,7239,3993],{"class":1385},[65,7241,1407],{"class":1389},[65,7243,7244],{"class":1410},"5s\n",[65,7246,7248,7251,7253],{"class":67,"line":7247},25,[65,7249,7250],{"class":1385}," timeout",[65,7252,1407],{"class":1389},[65,7254,7244],{"class":1410},[65,7256,7258,7261,7263],{"class":67,"line":7257},26,[65,7259,7260],{"class":1385}," retries",[65,7262,1407],{"class":1389},[65,7264,7265],{"class":1574},"5\n",[65,7267,7269],{"class":67,"line":7268},27,[65,7270,279],{"emptyLinePlaceholder":278},[65,7272,7274,7276],{"class":67,"line":7273},28,[65,7275,7138],{"class":1385},[65,7277,1390],{"class":1389},[65,7279,7281,7283,7285],{"class":67,"line":7280},29,[65,7282,7164],{"class":1385},[65,7284,1407],{"class":1389},[65,7286,7287],{"class":1410},"redis:7-alpine\n",[65,7289,7291],{"class":67,"line":7290},30,[65,7292,279],{"emptyLinePlaceholder":278},[65,7294,7296,7299],{"class":67,"line":7295},31,[65,7297,7298],{"class":1385},"Volumes",[65,7300,1390],{"class":1389},[65,7302,7304,7307],{"class":67,"line":7303},32,[65,7305,7306],{"class":1385}," postgres_data",[65,7308,1390],{"class":1389},[15,7310,185,7311,7314,7315,7318],{},[33,7312,7313],{},"depends_on"," with ",[33,7316,7317],{},"condition: service_healthy"," is critical. Without it, your API container starts before PostgreSQL is ready to accept connections, and your application crashes on boot. The healthcheck ensures Postgres is actually accepting queries before your API starts.",[15,7320,7321,7322,7325,7326,758],{},"Named volumes like ",[33,7323,7324],{},"postgres_data"," persist data between container restarts. Without a named volume, your database resets every time you run ",[33,7327,7328],{},"docker compose down",[22,7330,7332],{"id":7331},"environment-variables-and-secrets","Environment Variables and Secrets",[15,7334,7335],{},"Never bake secrets into your Docker image. Not in the Dockerfile, not in the Compose file checked into git. Use environment variables injected at runtime.",[15,7337,7338,7339,7342],{},"For local development, a ",[33,7340,7341],{},".env"," file works fine:",[56,7344,7347],{"className":7345,"code":7346,"language":3784},[3782],"DATABASE_URL=postgres://postgres:password@localhost:5432/myapp\nJWT_SECRET=dev-only-secret-not-for-production\n",[33,7348,7346],{"__ignoreMap":61},[15,7350,7351,7352,7354,7355,7358],{},"Add ",[33,7353,7341],{}," to your ",[33,7356,7357],{},".gitignore"," immediately. For production, use your platform's secret management: AWS Secrets Manager, Doppler, Vault, or at minimum environment variables set in your deployment configuration, not in your repository.",[15,7360,7361,7362,7365,7366,7369],{},"Docker supports a ",[33,7363,7364],{},"--env-file"," flag and Compose supports an ",[33,7367,7368],{},"env_file"," key. Use them. Your images should be configuration-agnostic, pulling their runtime values from the environment.",[22,7371,7373],{"id":7372},"common-mistakes-i-see-in-production","Common Mistakes I See in Production",[15,7375,7376,7379],{},[124,7377,7378],{},"Running as root."," By default, containers run as root. This is a security problem. Add a non-root user:",[56,7381,7383],{"className":6839,"code":7382,"language":6841,"meta":61,"style":61},"RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001\nUSER nodejs\n",[33,7384,7385,7390],{"__ignoreMap":61},[65,7386,7387],{"class":67,"line":68},[65,7388,7389],{},"RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001\n",[65,7391,7392],{"class":67,"line":74},[65,7393,7394],{},"USER nodejs\n",[15,7396,7397,7403,7404,7406,7407,2900,7409,7412,7413,2894],{},[124,7398,7399,7400,758],{},"No ",[33,7401,7402],{},".dockerignore"," Without a ",[33,7405,7402],{},", you send your entire project directory — including ",[33,7408,3751],{},[33,7410,7411],{},".git",", and test files — as the build context. Create a ",[33,7414,7402],{},[56,7416,7419],{"className":7417,"code":7418,"language":3784},[3782],"node_modules\n.git\n*.log\n.env\ndist\n",[33,7420,7418],{"__ignoreMap":61},[15,7422,7423,7426],{},[124,7424,7425],{},"Storing state in containers."," Containers are ephemeral. If your application writes files to the container's filesystem, those files disappear when the container restarts. Put file storage on a mounted volume or an object store like S3.",[15,7428,7429,7432],{},[124,7430,7431],{},"Not setting resource limits."," In production, always set memory and CPU limits. An unbounded container can starve other services on the same host. In Docker Compose:",[56,7434,7436],{"className":1370,"code":7435,"language":1372,"meta":61,"style":61},"deploy:\n resources:\n limits:\n cpus: \"0.5\"\n memory: 512M\n",[33,7437,7438,7444,7451,7458,7468],{"__ignoreMap":61},[65,7439,7440,7442],{"class":67,"line":68},[65,7441,1386],{"class":1385},[65,7443,1390],{"class":1389},[65,7445,7446,7449],{"class":67,"line":74},[65,7447,7448],{"class":1385}," resources",[65,7450,1390],{"class":1389},[65,7452,7453,7456],{"class":67,"line":80},[65,7454,7455],{"class":1385}," limits",[65,7457,1390],{"class":1389},[65,7459,7460,7463,7465],{"class":67,"line":86},[65,7461,7462],{"class":1385}," cpus",[65,7464,1407],{"class":1389},[65,7466,7467],{"class":1410},"\"0.5\"\n",[65,7469,7470,7473,7475],{"class":67,"line":92},[65,7471,7472],{"class":1385}," memory",[65,7474,1407],{"class":1389},[65,7476,7477],{"class":1410},"512M\n",[22,7479,7481],{"id":7480},"health-checks-in-production","Health Checks in Production",[15,7483,7484],{},"Every production container should expose a health check. Orchestrators like Kubernetes and ECS use health checks to determine if a container is ready to receive traffic and whether it needs to be restarted.",[56,7486,7488],{"className":6839,"code":7487,"language":6841,"meta":61,"style":61},"HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\\n CMD curl -f http://localhost:3000/health || exit 1\n",[33,7489,7490,7495],{"__ignoreMap":61},[65,7491,7492],{"class":67,"line":68},[65,7493,7494],{},"HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\\n",[65,7496,7497],{"class":67,"line":74},[65,7498,7499],{}," CMD curl -f http://localhost:3000/health || exit 1\n",[15,7501,7502,7503,7506],{},"Your ",[33,7504,7505],{},"/health"," endpoint should check actual application health — can it reach the database, is the cache connected — not just return a 200. A container that can't reach its database is not healthy, even if the process is running.",[22,7508,7510],{"id":7509},"the-path-forward","The Path Forward",[15,7512,7513],{},"Once you are comfortable with Docker basics, the natural progression is orchestration. Docker Compose handles multi-container local development. For production, you will eventually look at Kubernetes for complex deployments or managed container services like AWS ECS or Google Cloud Run for simpler ones.",[15,7515,7516],{},"But before you get there, internalize the fundamentals: keep images small, use multi-stage builds, never bake secrets into images, run as non-root, and treat containers as ephemeral. Get those right and you will avoid 90% of the production Docker problems I have seen.",[15,7518,7519],{},"Containerization is not optional anymore. It is the baseline expectation for professional software delivery in 2026. Start with one service, get it right, and expand from there.",[746,7521],{},[15,7523,7524,7525,758],{},"If you are building production infrastructure and want an experienced eye on your architecture, I would be glad to help. Book a session at ",[752,7526,754],{"href":754,"rel":7527},[756],[746,7529],{},[22,7531,764],{"id":763},[118,7533,7534,7540,7546,7552],{},[121,7535,7536],{},[752,7537,7539],{"href":7538},"/blog/kubernetes-basics-developers","Kubernetes for Application Developers: What You Actually Need to Know",[121,7541,7542],{},[752,7543,7545],{"href":7544},"/blog/container-security-guide","Container Security: Hardening Docker for Production",[121,7547,7548],{},[752,7549,7551],{"href":7550},"/blog/zero-to-production-nuxt-vercel","Zero to Production: My Nuxt + Vercel Deployment Pipeline",[121,7553,7554],{},[752,7555,7557],{"href":7556},"/blog/continuous-deployment-guide","Continuous Deployment: From Code Push to Production in Minutes",[792,7559,7560],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":61,"searchDepth":80,"depth":80,"links":7562},[7563,7564,7565,7566,7567,7568,7569,7570,7571],{"id":6815,"depth":74,"text":6816},{"id":6832,"depth":74,"text":6833},{"id":6941,"depth":74,"text":6942},{"id":7028,"depth":74,"text":7029},{"id":7331,"depth":74,"text":7332},{"id":7372,"depth":74,"text":7373},{"id":7480,"depth":74,"text":7481},{"id":7509,"depth":74,"text":7510},{"id":763,"depth":74,"text":764},"DevOps","A practical guide to Docker for developers — from writing your first Dockerfile to running production-grade containers with confidence.",[7575,7576],"Docker developers guide","Docker containerization",{},"/blog/docker-for-developers-guide",{"title":6800,"description":7573},"blog/docker-for-developers-guide",[7582,7583,7572,7584],"Docker","Containers","Backend","z7vTVMko0V1DPzhnZx7PVw28LgEweMyXmHotvh3ciEk",{"id":7587,"title":6756,"author":7588,"body":7589,"category":5607,"date":809,"description":7938,"extension":811,"featured":812,"image":813,"keywords":7939,"meta":7944,"navigation":278,"path":6755,"readTime":431,"seo":7945,"stem":7946,"tags":7947,"__hash__":7950},"blog/blog/domain-driven-design-guide.md",{"name":9,"bio":10},{"type":12,"value":7590,"toc":7923},[7591,7595,7598,7601,7604,7606,7610,7613,7624,7627,7630,7632,7636,7639,7650,7653,7674,7677,7679,7683,7686,7692,7698,7711,7715,7718,7732,7735,7737,7741,7748,7751,7755,7782,7786,7789,7792,7794,7798,7816,7819,7828,7848,7854,7856,7860,7863,7869,7875,7881,7887,7890,7892,7899,7901,7903],[22,7592,7594],{"id":7593},"the-theory-problem-with-ddd","The Theory Problem With DDD",[15,7596,7597],{},"Domain-Driven Design has a reputation problem. The canonical book is 560 pages. The community invented a vocabulary that sounds like it was designed to gatekeep: bounded contexts, aggregates, value objects, anti-corruption layers, context maps. Engineers encounter this terminology and conclude that DDD is an academic exercise, not a practical tool.",[15,7599,7600],{},"That's a shame, because the core ideas behind DDD are some of the most useful in software architecture. You don't need to absorb the entire theory to get significant value. You need to understand about five concepts and practice applying them to real code.",[15,7602,7603],{},"That's what this post covers.",[746,7605],{},[22,7607,7609],{"id":7608},"why-ddd-exists-the-problem-it-solves","Why DDD Exists (The Problem It Solves)",[15,7611,7612],{},"Before diving into concepts, it's worth understanding what problem DDD is designed to solve.",[15,7614,7615,7616,7619,7620,7623],{},"Most software systems fail at the same place: the gap between what the business needs and what the code models. Business experts speak one language; developers speak another. Over time, the code accumulates abstractions that make sense to engineers but map poorly to business reality. A \"customer\" in the billing module means something different from a \"customer\" in the CRM, but they're using the same ",[33,7617,7618],{},"Customer"," class. A \"product\" in the catalog is structured differently from a \"product\" in the warehouse, but there's a single ",[33,7621,7622],{},"Product"," table trying to serve both.",[15,7625,7626],{},"The result is software that's hard to change because any modification to a shared model breaks something unexpected, and hard to understand because the code doesn't reflect the business concepts it represents.",[15,7628,7629],{},"DDD's answer: model the software explicitly around the business domain, with language and structures that match how the business actually works. This sounds obvious. In practice, it requires discipline.",[746,7631],{},[22,7633,7635],{"id":7634},"the-ubiquitous-language","The Ubiquitous Language",[15,7637,7638],{},"The first and most impactful DDD concept requires no code at all.",[15,7640,7641,7642,7645,7646,7649],{},"A ",[124,7643,7644],{},"ubiquitous language"," is a shared vocabulary between developers and domain experts — business people, product managers, subject matter experts — where the terms mean the same thing in conversation, in documentation, and in the codebase. When a product manager says \"reservation\" and you say \"booking,\" and your database has a ",[33,7647,7648],{},"scheduled_appointment"," table, you have a language fragmentation problem. Changes get lost in translation. Misunderstandings accumulate.",[15,7651,7652],{},"Establishing ubiquitous language means:",[118,7654,7655,7658,7671],{},[121,7656,7657],{},"Using the domain expert's terms, not inventing developer-friendly synonyms",[121,7659,7660,7661,7664,7665,1998,7668,876],{},"Using the same terms in code as in conversation (the class is ",[33,7662,7663],{},"Reservation",", not ",[33,7666,7667],{},"Booking",[33,7669,7670],{},"Appointment",[121,7672,7673],{},"Correcting drift whenever it appears — when you discover the code says one thing and the business says another, fix the code",[15,7675,7676],{},"This is harder than it sounds because developers have a natural instinct to abstract and rename things. The discipline is to resist that instinct until you have a good reason to deviate from the domain expert's language.",[746,7678],{},[22,7680,7682],{"id":7681},"bounded-contexts","Bounded Contexts",[15,7684,7685],{},"This is the most powerful and most misunderstood concept in DDD.",[15,7687,7641,7688,7691],{},[124,7689,7690],{},"bounded context"," is an explicit boundary around a part of the system where a specific model and language apply. Inside that boundary, terms have precise meanings. Outside the boundary, the same term might mean something different.",[15,7693,7694,7695,7697],{},"The classic example: \"Customer\" in a sales context means a prospect being pursued. \"Customer\" in an order management context means someone who has placed an order. \"Customer\" in an accounting context means an entity with a billing relationship. These are related concepts, but they have different attributes, different behaviors, and different life cycles. Forcing them into a single ",[33,7696,7618],{}," model creates a bloated, confusing abstraction that serves none of these contexts well.",[15,7699,7700,7701,7704,7705,7707,7708,7710],{},"A bounded context gives each of these contexts its own model. The sales context has a ",[33,7702,7703],{},"Prospect",". The order context has a ",[33,7706,7618],{}," with an order history. The accounting context has an ",[33,7709,3220],{}," with billing terms. Each model is clean because it only needs to represent the things that matter in that context.",[938,7712,7714],{"id":7713},"identifying-bounded-contexts","Identifying Bounded Contexts",[15,7716,7717],{},"You find bounded context boundaries by looking for:",[118,7719,7720,7723,7726,7729],{},[121,7721,7722],{},"Places where the same word means different things to different teams",[121,7724,7725],{},"Teams that have genuinely different workflows around the same entity",[121,7727,7728],{},"Data that belongs entirely to one part of the organization and shouldn't leak to others",[121,7730,7731],{},"Natural friction points in the system where integration is awkward",[15,7733,7734],{},"In practice, bounded contexts often align reasonably well with organizational team structures — which is one of the things that makes microservices decomposition easier when you've done the DDD work first.",[746,7736],{},[22,7738,7740],{"id":7739},"aggregates","Aggregates",[15,7742,7743,7744,7747],{},"An ",[124,7745,7746],{},"aggregate"," is a cluster of domain objects treated as a single unit for data changes. Every aggregate has a root entity (the aggregate root) that controls all access to the objects within it.",[15,7749,7750],{},"The key rule: you only hold references to aggregate roots, never to objects inside an aggregate. And all changes to an aggregate go through the root.",[938,7752,7754],{"id":7753},"a-concrete-example","A Concrete Example",[15,7756,7757,7758,7760,7761,7763,7764,7767,7768,7770,7771,7773,7774,7777,7778,7781],{},"Consider an ",[33,7759,4967],{}," aggregate. An ",[33,7762,4967],{}," contains ",[33,7765,7766],{},"OrderLines",", each of which references a ",[33,7769,7622],{},". The ",[33,7772,4967],{}," is the aggregate root. To add a line item, you call ",[33,7775,7776],{},"order.addItem(product, quantity)"," — not ",[33,7779,7780],{},"order.items.push(new OrderLine(...))",". The root enforces business invariants: the order total stays consistent, the line item count doesn't exceed a limit, the order can't be modified after it's been shipped.",[938,7783,7785],{"id":7784},"why-this-matters","Why This Matters",[15,7787,7788],{},"Aggregates define the scope of transactional consistency. Within an aggregate, you have strong consistency — all changes happen together in a single transaction. Across aggregate boundaries, you rely on eventual consistency. This makes your consistency requirements explicit and limits the scope of each transaction, which is critical for scalability.",[15,7790,7791],{},"Size your aggregates carefully. Too large and you create contention and slow transactions. Too small and you push consistency requirements up to the application layer where they don't belong. The right size is the minimum cluster of objects that must be consistent together to enforce your business invariants.",[746,7793],{},[22,7795,7797],{"id":7796},"domain-events","Domain Events",[15,7799,7641,7800,7803,7804,2900,7806,2900,7809,2900,7812,7815],{},[124,7801,7802],{},"domain event"," is a record of something significant that happened in the domain. ",[33,7805,4974],{},[33,7807,7808],{},"PaymentDeclined",[33,7810,7811],{},"ItemShipped",[33,7813,7814],{},"CustomerUpgraded",". These are facts about the business that other parts of the system might need to know about.",[15,7817,7818],{},"Domain events serve two purposes:",[15,7820,7821,7824,7825,7827],{},[124,7822,7823],{},"Within the domain:"," They trigger side effects within the same bounded context. When an order is placed, inventory might need to be reserved. Modeling this as a domain event keeps the ",[33,7826,4967],{}," aggregate from needing to know about inventory.",[15,7829,7830,7833,7834,7837,7838,7770,7840,7843,7844,7847],{},[124,7831,7832],{},"Across bounded contexts:"," They communicate state changes to other bounded contexts without creating direct coupling. The ",[33,7835,7836],{},"OrderManagement"," context publishes ",[33,7839,4974],{},[33,7841,7842],{},"Warehouse"," context subscribes and begins picking. The ",[33,7845,7846],{},"Notifications"," context subscribes and sends a confirmation email. Each context responds to the same event independently.",[15,7849,7850,7851,7853],{},"The discipline is to make domain events explicit in your code — not just \"the order was saved to the database\" but \"the business fact ",[33,7852,4974],{}," occurred, and here is the data that fact carries.\"",[746,7855],{},[22,7857,7859],{"id":7858},"applying-ddd-without-going-all-in","Applying DDD Without Going All-In",[15,7861,7862],{},"You don't need to implement every DDD pattern to get value. Here's a pragmatic entry point:",[15,7864,7865,7868],{},[124,7866,7867],{},"Start with the language."," Before writing a line of code, sit with a domain expert and agree on the vocabulary. What are the key entities? What actions do they take? What triggers those actions? Document this. Enforce it in code reviews.",[15,7870,7871,7874],{},[124,7872,7873],{},"Identify one or two bounded contexts."," Don't try to map the whole system at once. Find the area of highest confusion — the place where the same data means different things in different places — and draw a clear context boundary there.",[15,7876,7877,7880],{},[124,7878,7879],{},"Model your aggregates explicitly."," Find the clusters of data that need to change together and give them clear roots. Push business rule enforcement into the aggregate, not into the service layer.",[15,7882,7883,7886],{},[124,7884,7885],{},"Raise domain events for important business facts."," When something significant happens, make it explicit with a domain event rather than burying it in a database transaction side effect.",[15,7888,7889],{},"DDD's value is proportional to your domain complexity. For CRUD applications with simple business rules, it's overkill. For complex domains with rich business logic, evolving requirements, and multiple teams — it's one of the most effective tools available.",[746,7891],{},[15,7893,7894,7895],{},"If you're working on a complex domain model and want to think through how DDD might apply, ",[752,7896,7898],{"href":754,"rel":7897},[756],"let's talk.",[746,7900],{},[22,7902,764],{"id":763},[118,7904,7905,7909,7915,7919],{},[121,7906,7907],{},[752,7908,4541],{"href":5616},[121,7910,7911],{},[752,7912,7914],{"href":7913},"/blog/cqrs-event-sourcing-explained","CQRS and Event Sourcing: A Practitioner's Honest Take",[121,7916,7917],{},[752,7918,5592],{"href":5591},[121,7920,7921],{},[752,7922,6762],{"href":6761},{"title":61,"searchDepth":80,"depth":80,"links":7924},[7925,7926,7927,7928,7931,7935,7936,7937],{"id":7593,"depth":74,"text":7594},{"id":7608,"depth":74,"text":7609},{"id":7634,"depth":74,"text":7635},{"id":7681,"depth":74,"text":7682,"children":7929},[7930],{"id":7713,"depth":80,"text":7714},{"id":7739,"depth":74,"text":7740,"children":7932},[7933,7934],{"id":7753,"depth":80,"text":7754},{"id":7784,"depth":80,"text":7785},{"id":7796,"depth":74,"text":7797},{"id":7858,"depth":74,"text":7859},{"id":763,"depth":74,"text":764},"Domain-driven design is often taught through dense theory. Here's how to apply DDD's most valuable concepts — bounded contexts, aggregates, domain events — to real projects without the philosophy degree.",[7940,7941,7942,7943,7644],"domain-driven design","bounded contexts","domain aggregates","DDD in practice",{},{"title":6756,"description":7938},"blog/domain-driven-design-guide",[7948,5621,5622,7949],"Domain-Driven Design","DDD","2x-CGGjIqTy8t-w7UWyCjeHXClNSKIN9WG_nOO0as3c",{"id":7952,"title":7953,"author":7954,"body":7955,"category":808,"date":809,"description":9096,"extension":811,"featured":812,"image":813,"keywords":9097,"meta":9100,"navigation":278,"path":9101,"readTime":104,"seo":9102,"stem":9103,"tags":9104,"__hash__":9107},"blog/blog/drizzle-orm-vs-prisma.md","Drizzle ORM vs Prisma: Which Should You Use in 2026?",{"name":9,"bio":10},{"type":12,"value":7956,"toc":9085},[7957,7960,7963,7967,7970,7973,7977,7980,8072,8075,8469,8472,8476,8479,8600,8603,8790,8793,8796,8810,8813,8817,8820,8823,8915,8918,8921,8924,8947,8950,8953,8971,8974,8977,8980,8983,8986,8990,8993,8996,9000,9007,9021,9026,9040,9043,9046,9048,9054,9056,9058,9082],[15,7958,7959],{},"A year ago, this comparison was easier — Prisma was the default choice for TypeScript ORMs and Drizzle was an interesting newcomer. In 2026, Drizzle has matured enough that the comparison is genuinely close, and the right choice depends on what you value most.",[15,7961,7962],{},"I use both in production. Here is what I have actually learned from that experience.",[22,7964,7966],{"id":7965},"what-each-solves","What Each Solves",[15,7968,7969],{},"Prisma's value proposition is developer experience. The schema DSL is declarative and readable, the client API is predictable, and the migration workflow is excellent. You define your data model in Prisma schema language and Prisma generates a fully-typed client that matches your schema exactly.",[15,7971,7972],{},"Drizzle's value proposition is SQL transparency. You write queries that look like SQL, the TypeScript inference is exceptional, and there is no query engine layer between your code and the database. If you can write the SQL, you can write the Drizzle.",[22,7974,7976],{"id":7975},"schema-definition","Schema Definition",[15,7978,7979],{},"Prisma schema:",[56,7981,7985],{"className":7982,"code":7983,"language":7984,"meta":61,"style":61},"language-prisma shiki shiki-themes github-dark","model User {\n id String @id @default(cuid())\n email String @unique\n name String?\n posts Post[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nModel Post {\n id String @id @default(cuid())\n title String\n content String\n published Boolean @default(false)\n author User @relation(fields: [authorId], references: [id])\n authorId String\n createdAt DateTime @default(now())\n}\n","prisma",[33,7986,7987,7992,7997,8002,8007,8012,8017,8022,8026,8030,8035,8039,8044,8049,8054,8059,8064,8068],{"__ignoreMap":61},[65,7988,7989],{"class":67,"line":68},[65,7990,7991],{},"model User {\n",[65,7993,7994],{"class":67,"line":74},[65,7995,7996],{}," id String @id @default(cuid())\n",[65,7998,7999],{"class":67,"line":80},[65,8000,8001],{}," email String @unique\n",[65,8003,8004],{"class":67,"line":86},[65,8005,8006],{}," name String?\n",[65,8008,8009],{"class":67,"line":92},[65,8010,8011],{}," posts Post[]\n",[65,8013,8014],{"class":67,"line":98},[65,8015,8016],{}," createdAt DateTime @default(now())\n",[65,8018,8019],{"class":67,"line":104},[65,8020,8021],{}," updatedAt DateTime @updatedAt\n",[65,8023,8024],{"class":67,"line":110},[65,8025,1952],{},[65,8027,8028],{"class":67,"line":367},[65,8029,279],{"emptyLinePlaceholder":278},[65,8031,8032],{"class":67,"line":431},[65,8033,8034],{},"Model Post {\n",[65,8036,8037],{"class":67,"line":713},[65,8038,7996],{},[65,8040,8041],{"class":67,"line":1043},[65,8042,8043],{}," title String\n",[65,8045,8046],{"class":67,"line":1049},[65,8047,8048],{}," content String\n",[65,8050,8051],{"class":67,"line":1190},[65,8052,8053],{}," published Boolean @default(false)\n",[65,8055,8056],{"class":67,"line":1196},[65,8057,8058],{}," author User @relation(fields: [authorId], references: [id])\n",[65,8060,8061],{"class":67,"line":1201},[65,8062,8063],{}," authorId String\n",[65,8065,8066],{"class":67,"line":1207},[65,8067,8016],{},[65,8069,8070],{"class":67,"line":1213},[65,8071,1952],{},[15,8073,8074],{},"Drizzle schema (PostgreSQL):",[56,8076,8078],{"className":1726,"code":8077,"language":1728,"meta":61,"style":61},"import { pgTable, text, boolean, timestamp } from 'drizzle-orm/pg-core'\nimport { relations } from 'drizzle-orm'\n\nExport const users = pgTable('users', {\n id: text('id').primaryKey().$defaultFn(() => createId()),\n email: text('email').notNull().unique(),\n name: text('name'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n})\n\nExport const posts = pgTable('posts', {\n id: text('id').primaryKey().$defaultFn(() => createId()),\n title: text('title').notNull(),\n content: text('content').notNull(),\n published: boolean('published').default(false).notNull(),\n authorId: text('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n})\n\nExport const usersRelations = relations(users, ({ many }) => ({\n posts: many(posts),\n}))\n",[33,8079,8080,8094,8106,8110,8132,8167,8192,8207,8231,8253,8257,8261,8280,8306,8324,8342,8370,8398,8418,8422,8426,8454,8464],{"__ignoreMap":61},[65,8081,8082,8085,8088,8091],{"class":67,"line":68},[65,8083,8084],{"class":1541},"import",[65,8086,8087],{"class":1389}," { pgTable, text, boolean, timestamp } ",[65,8089,8090],{"class":1541},"from",[65,8092,8093],{"class":1410}," 'drizzle-orm/pg-core'\n",[65,8095,8096,8098,8101,8103],{"class":67,"line":74},[65,8097,8084],{"class":1541},[65,8099,8100],{"class":1389}," { relations } ",[65,8102,8090],{"class":1541},[65,8104,8105],{"class":1410}," 'drizzle-orm'\n",[65,8107,8108],{"class":67,"line":80},[65,8109,279],{"emptyLinePlaceholder":278},[65,8111,8112,8115,8117,8119,8121,8124,8126,8129],{"class":67,"line":86},[65,8113,8114],{"class":1389},"Export ",[65,8116,1735],{"class":1541},[65,8118,1895],{"class":1574},[65,8120,1741],{"class":1541},[65,8122,8123],{"class":1534}," pgTable",[65,8125,1783],{"class":1389},[65,8127,8128],{"class":1410},"'users'",[65,8130,8131],{"class":1389},", {\n",[65,8133,8134,8137,8139,8141,8144,8147,8150,8153,8156,8159,8161,8164],{"class":67,"line":92},[65,8135,8136],{"class":1389}," id: ",[65,8138,3784],{"class":1534},[65,8140,1783],{"class":1389},[65,8142,8143],{"class":1410},"'id'",[65,8145,8146],{"class":1389},").",[65,8148,8149],{"class":1534},"primaryKey",[65,8151,8152],{"class":1389},"().",[65,8154,8155],{"class":1534},"$defaultFn",[65,8157,8158],{"class":1389},"(() ",[65,8160,1798],{"class":1541},[65,8162,8163],{"class":1534}," createId",[65,8165,8166],{"class":1389},"()),\n",[65,8168,8169,8172,8174,8176,8179,8181,8184,8186,8189],{"class":67,"line":98},[65,8170,8171],{"class":1389}," email: ",[65,8173,3784],{"class":1534},[65,8175,1783],{"class":1389},[65,8177,8178],{"class":1410},"'email'",[65,8180,8146],{"class":1389},[65,8182,8183],{"class":1534},"notNull",[65,8185,8152],{"class":1389},[65,8187,8188],{"class":1534},"unique",[65,8190,8191],{"class":1389},"(),\n",[65,8193,8194,8197,8199,8201,8204],{"class":67,"line":104},[65,8195,8196],{"class":1389}," name: ",[65,8198,3784],{"class":1534},[65,8200,1783],{"class":1389},[65,8202,8203],{"class":1410},"'name'",[65,8205,8206],{"class":1389},"),\n",[65,8208,8209,8212,8215,8217,8220,8222,8225,8227,8229],{"class":67,"line":110},[65,8210,8211],{"class":1389}," createdAt: ",[65,8213,8214],{"class":1534},"timestamp",[65,8216,1783],{"class":1389},[65,8218,8219],{"class":1410},"'created_at'",[65,8221,8146],{"class":1389},[65,8223,8224],{"class":1534},"defaultNow",[65,8226,8152],{"class":1389},[65,8228,8183],{"class":1534},[65,8230,8191],{"class":1389},[65,8232,8233,8236,8238,8240,8243,8245,8247,8249,8251],{"class":67,"line":367},[65,8234,8235],{"class":1389}," updatedAt: ",[65,8237,8214],{"class":1534},[65,8239,1783],{"class":1389},[65,8241,8242],{"class":1410},"'updated_at'",[65,8244,8146],{"class":1389},[65,8246,8224],{"class":1534},[65,8248,8152],{"class":1389},[65,8250,8183],{"class":1534},[65,8252,8191],{"class":1389},[65,8254,8255],{"class":67,"line":431},[65,8256,1772],{"class":1389},[65,8258,8259],{"class":67,"line":713},[65,8260,279],{"emptyLinePlaceholder":278},[65,8262,8263,8265,8267,8269,8271,8273,8275,8278],{"class":67,"line":1043},[65,8264,8114],{"class":1389},[65,8266,1735],{"class":1541},[65,8268,1935],{"class":1574},[65,8270,1741],{"class":1541},[65,8272,8123],{"class":1534},[65,8274,1783],{"class":1389},[65,8276,8277],{"class":1410},"'posts'",[65,8279,8131],{"class":1389},[65,8281,8282,8284,8286,8288,8290,8292,8294,8296,8298,8300,8302,8304],{"class":67,"line":1049},[65,8283,8136],{"class":1389},[65,8285,3784],{"class":1534},[65,8287,1783],{"class":1389},[65,8289,8143],{"class":1410},[65,8291,8146],{"class":1389},[65,8293,8149],{"class":1534},[65,8295,8152],{"class":1389},[65,8297,8155],{"class":1534},[65,8299,8158],{"class":1389},[65,8301,1798],{"class":1541},[65,8303,8163],{"class":1534},[65,8305,8166],{"class":1389},[65,8307,8308,8311,8313,8315,8318,8320,8322],{"class":67,"line":1190},[65,8309,8310],{"class":1389}," title: ",[65,8312,3784],{"class":1534},[65,8314,1783],{"class":1389},[65,8316,8317],{"class":1410},"'title'",[65,8319,8146],{"class":1389},[65,8321,8183],{"class":1534},[65,8323,8191],{"class":1389},[65,8325,8326,8329,8331,8333,8336,8338,8340],{"class":67,"line":1196},[65,8327,8328],{"class":1389}," content: ",[65,8330,3784],{"class":1534},[65,8332,1783],{"class":1389},[65,8334,8335],{"class":1410},"'content'",[65,8337,8146],{"class":1389},[65,8339,8183],{"class":1534},[65,8341,8191],{"class":1389},[65,8343,8344,8347,8350,8352,8355,8357,8360,8362,8364,8366,8368],{"class":67,"line":1201},[65,8345,8346],{"class":1389}," published: ",[65,8348,8349],{"class":1534},"boolean",[65,8351,1783],{"class":1389},[65,8353,8354],{"class":1410},"'published'",[65,8356,8146],{"class":1389},[65,8358,8359],{"class":1534},"default",[65,8361,1783],{"class":1389},[65,8363,3472],{"class":1574},[65,8365,8146],{"class":1389},[65,8367,8183],{"class":1534},[65,8369,8191],{"class":1389},[65,8371,8372,8375,8377,8379,8382,8384,8386,8388,8391,8393,8395],{"class":67,"line":1207},[65,8373,8374],{"class":1389}," authorId: ",[65,8376,3784],{"class":1534},[65,8378,1783],{"class":1389},[65,8380,8381],{"class":1410},"'author_id'",[65,8383,8146],{"class":1389},[65,8385,8183],{"class":1534},[65,8387,8152],{"class":1389},[65,8389,8390],{"class":1534},"references",[65,8392,8158],{"class":1389},[65,8394,1798],{"class":1541},[65,8396,8397],{"class":1389}," users.id),\n",[65,8399,8400,8402,8404,8406,8408,8410,8412,8414,8416],{"class":67,"line":1213},[65,8401,8211],{"class":1389},[65,8403,8214],{"class":1534},[65,8405,1783],{"class":1389},[65,8407,8219],{"class":1410},[65,8409,8146],{"class":1389},[65,8411,8224],{"class":1534},[65,8413,8152],{"class":1389},[65,8415,8183],{"class":1534},[65,8417,8191],{"class":1389},[65,8419,8420],{"class":67,"line":1219},[65,8421,1772],{"class":1389},[65,8423,8424],{"class":67,"line":3094},[65,8425,279],{"emptyLinePlaceholder":278},[65,8427,8428,8430,8432,8435,8437,8440,8443,8446,8449,8451],{"class":67,"line":3099},[65,8429,8114],{"class":1389},[65,8431,1735],{"class":1541},[65,8433,8434],{"class":1574}," usersRelations",[65,8436,1741],{"class":1541},[65,8438,8439],{"class":1534}," relations",[65,8441,8442],{"class":1389},"(users, ({ ",[65,8444,8445],{"class":1791},"many",[65,8447,8448],{"class":1389}," }) ",[65,8450,1798],{"class":1541},[65,8452,8453],{"class":1389}," ({\n",[65,8455,8456,8459,8461],{"class":67,"line":3104},[65,8457,8458],{"class":1389}," posts: ",[65,8460,8445],{"class":1534},[65,8462,8463],{"class":1389},"(posts),\n",[65,8465,8466],{"class":67,"line":3122},[65,8467,8468],{"class":1389},"}))\n",[15,8470,8471],{},"The Prisma schema is more concise and arguably more readable. Drizzle's schema is TypeScript — no custom DSL to learn, no Prisma LSP required for syntax highlighting.",[22,8473,8475],{"id":8474},"query-api","Query API",[15,8477,8478],{},"Prisma:",[56,8480,8482],{"className":1726,"code":8481,"language":1728,"meta":61,"style":61},"// Find users with their posts\nconst users = await prisma.user.findMany({\n where: {\n posts: { some: { published: true } },\n },\n include: {\n posts: {\n where: { published: true },\n orderBy: { createdAt: 'desc' },\n take: 5,\n },\n },\n orderBy: { createdAt: 'desc' },\n take: 20,\n skip: 0,\n})\n",[33,8483,8484,8489,8505,8509,8519,8523,8528,8533,8542,8552,8562,8566,8570,8578,8587,8596],{"__ignoreMap":61},[65,8485,8486],{"class":67,"line":68},[65,8487,8488],{"class":1379},"// Find users with their posts\n",[65,8490,8491,8493,8495,8497,8499,8501,8503],{"class":67,"line":74},[65,8492,1735],{"class":1541},[65,8494,1895],{"class":1574},[65,8496,1741],{"class":1541},[65,8498,1900],{"class":1541},[65,8500,1903],{"class":1389},[65,8502,1906],{"class":1534},[65,8504,1750],{"class":1389},[65,8506,8507],{"class":67,"line":80},[65,8508,3010],{"class":1389},[65,8510,8511,8514,8516],{"class":67,"line":86},[65,8512,8513],{"class":1389}," posts: { some: { published: ",[65,8515,1985],{"class":1574},[65,8517,8518],{"class":1389}," } },\n",[65,8520,8521],{"class":67,"line":92},[65,8522,2790],{"class":1389},[65,8524,8525],{"class":67,"line":98},[65,8526,8527],{"class":1389}," include: {\n",[65,8529,8530],{"class":67,"line":104},[65,8531,8532],{"class":1389}," posts: {\n",[65,8534,8535,8538,8540],{"class":67,"line":110},[65,8536,8537],{"class":1389}," where: { published: ",[65,8539,1985],{"class":1574},[65,8541,2790],{"class":1389},[65,8543,8544,8547,8550],{"class":67,"line":367},[65,8545,8546],{"class":1389}," orderBy: { createdAt: ",[65,8548,8549],{"class":1410},"'desc'",[65,8551,2790],{"class":1389},[65,8553,8554,8557,8560],{"class":67,"line":431},[65,8555,8556],{"class":1389}," take: ",[65,8558,8559],{"class":1574},"5",[65,8561,3909],{"class":1389},[65,8563,8564],{"class":67,"line":713},[65,8565,2790],{"class":1389},[65,8567,8568],{"class":67,"line":1043},[65,8569,2790],{"class":1389},[65,8571,8572,8574,8576],{"class":67,"line":1049},[65,8573,8546],{"class":1389},[65,8575,8549],{"class":1410},[65,8577,2790],{"class":1389},[65,8579,8580,8582,8585],{"class":67,"line":1190},[65,8581,8556],{"class":1389},[65,8583,8584],{"class":1574},"20",[65,8586,3909],{"class":1389},[65,8588,8589,8592,8594],{"class":67,"line":1196},[65,8590,8591],{"class":1389}," skip: ",[65,8593,3255],{"class":1574},[65,8595,3909],{"class":1389},[65,8597,8598],{"class":67,"line":1201},[65,8599,1772],{"class":1389},[15,8601,8602],{},"Drizzle:",[56,8604,8606],{"className":1726,"code":8605,"language":1728,"meta":61,"style":61},"// Same query in Drizzle\nconst result = await db\n .select({\n user: users,\n post: posts,\n })\n .from(users)\n .leftJoin(posts, and(\n eq(posts.authorId, users.id),\n eq(posts.published, true)\n ))\n .where(\n inArray(users.id,\n db.select({ id: posts.authorId })\n .from(posts)\n .where(eq(posts.published, true))\n )\n )\n .orderBy(desc(users.createdAt))\n .limit(20)\n",[33,8607,8608,8613,8627,8637,8642,8647,8651,8660,8675,8683,8694,8699,8708,8716,8726,8735,8753,8758,8762,8777],{"__ignoreMap":61},[65,8609,8610],{"class":67,"line":68},[65,8611,8612],{"class":1379},"// Same query in Drizzle\n",[65,8614,8615,8617,8620,8622,8624],{"class":67,"line":74},[65,8616,1735],{"class":1541},[65,8618,8619],{"class":1574}," result",[65,8621,1741],{"class":1541},[65,8623,1900],{"class":1541},[65,8625,8626],{"class":1389}," db\n",[65,8628,8629,8632,8635],{"class":67,"line":80},[65,8630,8631],{"class":1389}," .",[65,8633,8634],{"class":1534},"select",[65,8636,1750],{"class":1389},[65,8638,8639],{"class":67,"line":86},[65,8640,8641],{"class":1389}," user: users,\n",[65,8643,8644],{"class":67,"line":92},[65,8645,8646],{"class":1389}," post: posts,\n",[65,8648,8649],{"class":67,"line":98},[65,8650,2983],{"class":1389},[65,8652,8653,8655,8657],{"class":67,"line":104},[65,8654,8631],{"class":1389},[65,8656,8090],{"class":1534},[65,8658,8659],{"class":1389},"(users)\n",[65,8661,8662,8664,8667,8670,8673],{"class":67,"line":110},[65,8663,8631],{"class":1389},[65,8665,8666],{"class":1534},"leftJoin",[65,8668,8669],{"class":1389},"(posts, ",[65,8671,8672],{"class":1534},"and",[65,8674,2764],{"class":1389},[65,8676,8677,8680],{"class":67,"line":367},[65,8678,8679],{"class":1534}," eq",[65,8681,8682],{"class":1389},"(posts.authorId, users.id),\n",[65,8684,8685,8687,8690,8692],{"class":67,"line":431},[65,8686,8679],{"class":1534},[65,8688,8689],{"class":1389},"(posts.published, ",[65,8691,1985],{"class":1574},[65,8693,1857],{"class":1389},[65,8695,8696],{"class":67,"line":713},[65,8697,8698],{"class":1389}," ))\n",[65,8700,8701,8703,8706],{"class":67,"line":1043},[65,8702,8631],{"class":1389},[65,8704,8705],{"class":1534},"where",[65,8707,2764],{"class":1389},[65,8709,8710,8713],{"class":67,"line":1049},[65,8711,8712],{"class":1534}," inArray",[65,8714,8715],{"class":1389},"(users.id,\n",[65,8717,8718,8721,8723],{"class":67,"line":1190},[65,8719,8720],{"class":1389}," db.",[65,8722,8634],{"class":1534},[65,8724,8725],{"class":1389},"({ id: posts.authorId })\n",[65,8727,8728,8730,8732],{"class":67,"line":1196},[65,8729,8631],{"class":1389},[65,8731,8090],{"class":1534},[65,8733,8734],{"class":1389},"(posts)\n",[65,8736,8737,8739,8741,8743,8746,8748,8750],{"class":67,"line":1201},[65,8738,8631],{"class":1389},[65,8740,8705],{"class":1534},[65,8742,1783],{"class":1389},[65,8744,8745],{"class":1534},"eq",[65,8747,8689],{"class":1389},[65,8749,1985],{"class":1574},[65,8751,8752],{"class":1389},"))\n",[65,8754,8755],{"class":67,"line":1207},[65,8756,8757],{"class":1389}," )\n",[65,8759,8760],{"class":67,"line":1213},[65,8761,8757],{"class":1389},[65,8763,8764,8766,8769,8771,8774],{"class":67,"line":1219},[65,8765,8631],{"class":1389},[65,8767,8768],{"class":1534},"orderBy",[65,8770,1783],{"class":1389},[65,8772,8773],{"class":1534},"desc",[65,8775,8776],{"class":1389},"(users.createdAt))\n",[65,8778,8779,8781,8784,8786,8788],{"class":67,"line":3094},[65,8780,8631],{"class":1389},[65,8782,8783],{"class":1534},"limit",[65,8785,1783],{"class":1389},[65,8787,8584],{"class":1574},[65,8789,1857],{"class":1389},[15,8791,8792],{},"Prisma's API is higher-level and more readable for common patterns. Drizzle's is closer to SQL — more verbose for simple queries, but more expressive for complex ones.",[15,8794,8795],{},"The Drizzle advantage becomes clear for:",[118,8797,8798,8801,8804,8807],{},[121,8799,8800],{},"Complex joins with multiple conditions",[121,8802,8803],{},"Window functions",[121,8805,8806],{},"CTEs (Common Table Expressions)",[121,8808,8809],{},"Database-specific features",[15,8811,8812],{},"Prisma forces you to drop to raw SQL for complex queries. Drizzle keeps everything in TypeScript with full type inference.",[22,8814,8816],{"id":8815},"type-safety","Type Safety",[15,8818,8819],{},"Both have excellent TypeScript support, but Drizzle's is more comprehensive. Drizzle infers the exact shape of every query result based on the columns you select. Prisma infers based on your include/select configuration.",[15,8821,8822],{},"The difference shows up with partial selects:",[56,8824,8826],{"className":1726,"code":8825,"language":1728,"meta":61,"style":61},"// Drizzle: exact type inference for partial selects\nconst result = await db\n .select({ id: users.id, email: users.email })\n .from(users)\n// result is { id: string; email: string }[]\n\n// Prisma: requires explicit select typing\nconst result = await prisma.user.findMany({\n select: { id: true, email: true },\n})\n// result is { id: string; email: string }[] — also works\n",[33,8827,8828,8833,8845,8854,8862,8867,8871,8876,8892,8906,8910],{"__ignoreMap":61},[65,8829,8830],{"class":67,"line":68},[65,8831,8832],{"class":1379},"// Drizzle: exact type inference for partial selects\n",[65,8834,8835,8837,8839,8841,8843],{"class":67,"line":74},[65,8836,1735],{"class":1541},[65,8838,8619],{"class":1574},[65,8840,1741],{"class":1541},[65,8842,1900],{"class":1541},[65,8844,8626],{"class":1389},[65,8846,8847,8849,8851],{"class":67,"line":80},[65,8848,8631],{"class":1389},[65,8850,8634],{"class":1534},[65,8852,8853],{"class":1389},"({ id: users.id, email: users.email })\n",[65,8855,8856,8858,8860],{"class":67,"line":86},[65,8857,8631],{"class":1389},[65,8859,8090],{"class":1534},[65,8861,8659],{"class":1389},[65,8863,8864],{"class":67,"line":92},[65,8865,8866],{"class":1379},"// result is { id: string; email: string }[]\n",[65,8868,8869],{"class":67,"line":98},[65,8870,279],{"emptyLinePlaceholder":278},[65,8872,8873],{"class":67,"line":104},[65,8874,8875],{"class":1379},"// Prisma: requires explicit select typing\n",[65,8877,8878,8880,8882,8884,8886,8888,8890],{"class":67,"line":110},[65,8879,1735],{"class":1541},[65,8881,8619],{"class":1574},[65,8883,1741],{"class":1541},[65,8885,1900],{"class":1541},[65,8887,1903],{"class":1389},[65,8889,1906],{"class":1534},[65,8891,1750],{"class":1389},[65,8893,8894,8897,8899,8902,8904],{"class":67,"line":367},[65,8895,8896],{"class":1389}," select: { id: ",[65,8898,1985],{"class":1574},[65,8900,8901],{"class":1389},", email: ",[65,8903,1985],{"class":1574},[65,8905,2790],{"class":1389},[65,8907,8908],{"class":67,"line":431},[65,8909,1772],{"class":1389},[65,8911,8912],{"class":67,"line":713},[65,8913,8914],{"class":1379},"// result is { id: string; email: string }[] — also works\n",[15,8916,8917],{},"Both handle this case. Drizzle pulls ahead for complex JOIN queries where Prisma's type inference sometimes loses precision.",[22,8919,1653],{"id":8920},"migrations",[15,8922,8923],{},"Prisma migrations are excellent. The workflow is:",[2396,8925,8926,8932,8938,8941],{},[121,8927,8928,8929],{},"Edit ",[33,8930,8931],{},"schema.prisma",[121,8933,2400,8934,8937],{},[33,8935,8936],{},"prisma migrate dev"," — generates SQL migration file and applies it",[121,8939,8940],{},"Commit the migration file with your code changes",[121,8942,8943,8946],{},[33,8944,8945],{},"prisma migrate deploy"," runs in production CI",[15,8948,8949],{},"The generated migrations are readable SQL that you review before applying. The migration history is stored in the database and tracked in your repository. It is a mature, reliable workflow.",[15,8951,8952],{},"Drizzle migrations are newer but functional:",[2396,8954,8955,8958,8964],{},[121,8956,8957],{},"Edit your schema TypeScript file",[121,8959,2400,8960,8963],{},[33,8961,8962],{},"drizzle-kit generate"," — generates SQL migration file",[121,8965,8966,8967,8970],{},"Apply with ",[33,8968,8969],{},"drizzle-kit migrate"," or in your application startup",[15,8972,8973],{},"The Drizzle Kit tooling has improved substantially but still occasionally generates migrations that need manual review before applying in production. For production-critical databases, I am more confident in Prisma's migration reliability.",[22,8975,824],{"id":8976},"performance",[15,8978,8979],{},"Drizzle is measurably faster for most queries. It generates more efficient SQL, has no intermediate query engine, and has lower startup overhead. The difference is typically in the range of 20-50% for simple queries.",[15,8981,8982],{},"For most applications, this does not matter. Database query latency and network latency dwarf ORM overhead. But for high-throughput APIs (hundreds of queries per second), or applications running in edge environments where startup time matters, Drizzle's performance advantage is real.",[15,8984,8985],{},"Prisma's Accelerate product addresses the performance concern with connection pooling and query caching, but it adds another service to your infrastructure.",[22,8987,8989],{"id":8988},"edge-compatibility","Edge Compatibility",[15,8991,8992],{},"Drizzle works in edge environments (Cloudflare Workers, Vercel Edge Functions) with compatible database drivers. Prisma requires Node.js — the Prisma Client does not work in edge runtimes.",[15,8994,8995],{},"If you are deploying to Cloudflare Pages with edge SSR or Vercel Edge Functions, Drizzle is currently the only ORM option.",[22,8997,8999],{"id":8998},"my-current-decision-framework","My Current Decision Framework",[15,9001,9002,9003,9006],{},"I use ",[124,9004,9005],{},"Prisma"," when:",[118,9008,9009,9012,9015,9018],{},[121,9010,9011],{},"The team has TypeScript backend developers who are not database experts",[121,9013,9014],{},"The migration workflow needs to be bulletproof",[121,9016,9017],{},"Node.js deployment (no edge requirement)",[121,9019,9020],{},"Rapid prototyping where developer experience matters most",[15,9022,9002,9023,9006],{},[124,9024,9025],{},"Drizzle",[118,9027,9028,9031,9034,9037],{},[121,9029,9030],{},"Deploying to edge environments",[121,9032,9033],{},"Performance is a hard requirement",[121,9035,9036],{},"The team is comfortable with SQL and wants to stay close to it",[121,9038,9039],{},"Complex queries are the norm",[15,9041,9042],{},"For greenfield projects where I control all the variables and edge deployment is on the roadmap, I am increasingly starting with Drizzle. The TypeScript ergonomics have improved to the point where the schema definition and query API are genuinely pleasant.",[15,9044,9045],{},"For enterprise projects where reliability and team familiarity are paramount, Prisma remains my default.",[746,9047],{},[15,9049,9050,9051,758],{},"Choosing between Drizzle and Prisma for your project, or evaluating your existing ORM choice? Book a call and we can think through the trade-offs for your specific situation: ",[752,9052,757],{"href":754,"rel":9053},[756],[746,9055],{},[22,9057,764],{"id":763},[118,9059,9060,9066,9072,9078],{},[121,9061,9062],{},[752,9063,9065],{"href":9064},"/blog/prisma-orm-guide","Prisma ORM: A Complete Guide for TypeScript Developers",[121,9067,9068],{},[752,9069,9071],{"href":9070},"/blog/typescript-backend-development","TypeScript for Backend Development: Patterns I Use on Every Project",[121,9073,9074],{},[752,9075,9077],{"href":9076},"/blog/building-rest-apis-typescript","Building REST APIs With TypeScript: Patterns From Production",[121,9079,9080],{},[752,9081,772],{"href":771},[792,9083,9084],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":80,"depth":80,"links":9086},[9087,9088,9089,9090,9091,9092,9093,9094,9095],{"id":7965,"depth":74,"text":7966},{"id":7975,"depth":74,"text":7976},{"id":8474,"depth":74,"text":8475},{"id":8815,"depth":74,"text":8816},{"id":8920,"depth":74,"text":1653},{"id":8976,"depth":74,"text":824},{"id":8988,"depth":74,"text":8989},{"id":8998,"depth":74,"text":8999},{"id":763,"depth":74,"text":764},"An in-depth comparison of Drizzle ORM and Prisma for TypeScript developers — query syntax, performance, migrations, type safety, and when each fits your project.",[9098,9099],"Drizzle ORM vs Prisma","ORM comparison",{},"/blog/drizzle-orm-vs-prisma",{"title":7953,"description":9096},"blog/drizzle-orm-vs-prisma",[822,9105,9106],"ORM","TypeScript","aUbpS4O89JMuAyzTHokiZTTrIv7nhD4NMEwY3I8Sa88",{"id":9109,"title":9110,"author":9111,"body":9112,"category":7572,"date":809,"description":10630,"extension":811,"featured":812,"image":813,"keywords":10631,"meta":10637,"navigation":278,"path":10638,"readTime":431,"seo":10639,"stem":10640,"tags":10641,"__hash__":10646},"blog/blog/edge-computing-cloudflare-workers.md","Edge Computing with Cloudflare Workers: Moving Logic to Where Your Users Are",{"name":9,"bio":10},{"type":12,"value":9113,"toc":10606},[9114,9117,9120,9123,9127,9130,9133,9136,9140,9143,9147,9150,9154,9157,9161,9164,9168,9176,9180,9183,9186,9189,9590,9593,9597,9601,9608,9945,9949,10443,10446,10450,10453,10459,10465,10471,10481,10487,10491,10495,10498,10502,10505,10509,10512,10516,10530,10534,10537,10548,10554,10560,10566,10570,10573,10576,10578,10580,10603],[3743,9115,9110],{"id":9116},"edge-computing-with-cloudflare-workers-moving-logic-to-where-your-users-are",[15,9118,9119],{},"Most developers hear \"edge computing\" and think of CDNs caching static files. That is a piece of the picture, but it misses the more interesting part. Edge computing is about running your actual application logic — authentication checks, request routing, data transformations — on servers physically close to the user making the request. Not in a single us-east-1 region. Not behind a load balancer pointing to three availability zones in Virginia. On a machine in the same city as the person clicking the button.",[15,9121,9122],{},"I have been running production workloads on Cloudflare Workers for over a year now. Some of those decisions were obvious wins. Others taught me where the edge falls apart. This is the honest breakdown.",[22,9124,9126],{"id":9125},"what-edge-computing-actually-is","What Edge Computing Actually Is",[15,9128,9129],{},"A traditional server architecture looks like this: a user in Tokyo makes a request, that request crosses the Pacific Ocean, hits your load balancer in Oregon, gets routed to an application server, queries a database, and sends the response back across the Pacific. Round trip latency of 150 to 300 milliseconds before your code even runs.",[15,9131,9132],{},"Edge computing flips this. Your code runs on a network of globally distributed nodes — Cloudflare has over 300 points of presence worldwide. When the user in Tokyo makes that request, it hits a Cloudflare node in Tokyo. Your code executes there. If it can resolve the request without calling back to an origin server, the response is back to the user in single-digit milliseconds.",[15,9134,9135],{},"The key distinction from a CDN is that you are running arbitrary code, not just serving cached files. A CDN answers \"here is a copy of that HTML file.\" An edge function answers \"let me check your auth token, look up your geolocation, decide which variant of the page you should see, and return a response.\" That is a fundamentally different capability.",[22,9137,9139],{"id":9138},"when-edge-makes-sense","When Edge Makes Sense",[15,9141,9142],{},"Not every piece of logic belongs at the edge. The sweet spot is operations that are latency-sensitive, relatively lightweight, and do not require complex database transactions. Here is where I consistently reach for Workers.",[938,9144,9146],{"id":9145},"authentication-and-authorization","Authentication and Authorization",[15,9148,9149],{},"Validating a JWT or session token is a perfect edge operation. The token is in the request headers. Validation is a cryptographic operation that does not require a database call. If the token is invalid, you reject the request at the edge before it ever touches your origin — saving compute and reducing attack surface.",[938,9151,9153],{"id":9152},"geolocation-routing","Geolocation Routing",[15,9155,9156],{},"Cloudflare attaches geolocation data to every request automatically. Redirecting users to region-specific content, enforcing geographic compliance rules, or serving localized pricing — all of this runs naturally at the edge without external API calls.",[938,9158,9160],{"id":9159},"ab-testing-and-feature-flags","A/B Testing and Feature Flags",[15,9162,9163],{},"Instead of loading a feature flag SDK on the client that makes its own network requests, resolve the flag at the edge. Read the experiment assignment from KV storage, modify the response, and the user never knows the decision happened. No layout shift, no flicker, no additional round trip.",[938,9165,9167],{"id":9166},"request-rewriting-and-middleware","Request Rewriting and Middleware",[15,9169,9170,9171,9175],{},"URL rewrites, header injection, CORS handling, rate limiting — these are operations that happen on every request and benefit enormously from running close to the user. I wrote about how this fits into the broader deployment picture in my ",[752,9172,9174],{"href":9173},"/blog/cloudflare-pages-guide","Cloudflare Pages guide",", where Workers handle the server-side logic alongside static frontends.",[22,9177,9179],{"id":9178},"cloudflare-workers-basics","Cloudflare Workers Basics",[15,9181,9182],{},"Workers run in V8 isolates — the same JavaScript engine that powers Chrome, but without a full browser or Node.js environment. Startup time is under a millisecond because there is no cold container to provision. You write standard TypeScript, deploy with Wrangler, and Cloudflare handles the distribution.",[15,9184,9185],{},"The ecosystem has matured significantly. You can use Hono as a lightweight framework on top of Workers, connect to KV for global key-value storage, D1 for SQLite-based relational data, and R2 for S3-compatible object storage. The entire stack runs on Cloudflare's network without reaching out to AWS or GCP.",[15,9187,9188],{},"Here is a minimal Worker with Hono that handles routing:",[56,9190,9192],{"className":1726,"code":9191,"language":1728,"meta":61,"style":61},"import { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { jwt } from \"hono/jwt\";\n\nType Bindings = {\n CACHE: KVNamespace;\n DB: D1Database;\n JWT_SECRET: string;\n};\n\nConst app = new Hono\u003C{ Bindings: Bindings }>();\n\nApp.use(\"/api/*\", cors());\napp.use(\"/api/protected/*\", jwt({ secret: (c) => c.env.JWT_SECRET }));\n\nApp.get(\"/api/products\", async (c) => {\n const cached = await c.env.CACHE.get(\"products:all\", \"json\");\n if (cached) {\n return c.json(cached);\n }\n\n const { results } = await c.env.DB.prepare(\n \"SELECT id, name, price FROM products WHERE active = 1\"\n ).all();\n\n await c.env.CACHE.put(\"products:all\", JSON.stringify(results), {\n expirationTtl: 300,\n });\n\n return c.json(results);\n});\n\nExport default app;\n",[33,9193,9194,9209,9223,9237,9241,9250,9255,9263,9271,9276,9280,9306,9310,9331,9373,9377,9403,9436,9443,9455,9459,9463,9491,9496,9507,9511,9541,9551,9556,9560,9571,9576,9580],{"__ignoreMap":61},[65,9195,9196,9198,9201,9203,9206],{"class":67,"line":68},[65,9197,8084],{"class":1541},[65,9199,9200],{"class":1389}," { Hono } ",[65,9202,8090],{"class":1541},[65,9204,9205],{"class":1410}," \"hono\"",[65,9207,9208],{"class":1389},";\n",[65,9210,9211,9213,9216,9218,9221],{"class":67,"line":74},[65,9212,8084],{"class":1541},[65,9214,9215],{"class":1389}," { cors } ",[65,9217,8090],{"class":1541},[65,9219,9220],{"class":1410}," \"hono/cors\"",[65,9222,9208],{"class":1389},[65,9224,9225,9227,9230,9232,9235],{"class":67,"line":80},[65,9226,8084],{"class":1541},[65,9228,9229],{"class":1389}," { jwt } ",[65,9231,8090],{"class":1541},[65,9233,9234],{"class":1410}," \"hono/jwt\"",[65,9236,9208],{"class":1389},[65,9238,9239],{"class":67,"line":86},[65,9240,279],{"emptyLinePlaceholder":278},[65,9242,9243,9246,9248],{"class":67,"line":92},[65,9244,9245],{"class":1389},"Type Bindings ",[65,9247,2937],{"class":1541},[65,9249,1801],{"class":1389},[65,9251,9252],{"class":67,"line":98},[65,9253,9254],{"class":1389}," CACHE: KVNamespace;\n",[65,9256,9257,9260],{"class":67,"line":104},[65,9258,9259],{"class":1574}," DB",[65,9261,9262],{"class":1389},": D1Database;\n",[65,9264,9265,9268],{"class":67,"line":110},[65,9266,9267],{"class":1574}," JWT_SECRET",[65,9269,9270],{"class":1389},": string;\n",[65,9272,9273],{"class":67,"line":367},[65,9274,9275],{"class":1389},"};\n",[65,9277,9278],{"class":67,"line":431},[65,9279,279],{"emptyLinePlaceholder":278},[65,9281,9282,9285,9287,9289,9292,9295,9298,9300,9303],{"class":67,"line":713},[65,9283,9284],{"class":1389},"Const app ",[65,9286,2937],{"class":1541},[65,9288,1744],{"class":1541},[65,9290,9291],{"class":1534}," Hono",[65,9293,9294],{"class":1389},"\u003C{ ",[65,9296,9297],{"class":1791},"Bindings",[65,9299,2894],{"class":1541},[65,9301,9302],{"class":1534}," Bindings",[65,9304,9305],{"class":1389}," }>();\n",[65,9307,9308],{"class":67,"line":1043},[65,9309,279],{"emptyLinePlaceholder":278},[65,9311,9312,9315,9318,9320,9323,9325,9328],{"class":67,"line":1049},[65,9313,9314],{"class":1389},"App.",[65,9316,9317],{"class":1534},"use",[65,9319,1783],{"class":1389},[65,9321,9322],{"class":1410},"\"/api/*\"",[65,9324,2900],{"class":1389},[65,9326,9327],{"class":1534},"cors",[65,9329,9330],{"class":1389},"());\n",[65,9332,9333,9336,9338,9340,9343,9345,9348,9351,9354,9357,9360,9362,9364,9367,9370],{"class":67,"line":1190},[65,9334,9335],{"class":1389},"app.",[65,9337,9317],{"class":1534},[65,9339,1783],{"class":1389},[65,9341,9342],{"class":1410},"\"/api/protected/*\"",[65,9344,2900],{"class":1389},[65,9346,9347],{"class":1534},"jwt",[65,9349,9350],{"class":1389},"({ ",[65,9352,9353],{"class":1534},"secret",[65,9355,9356],{"class":1389},": (",[65,9358,9359],{"class":1791},"c",[65,9361,1795],{"class":1389},[65,9363,1798],{"class":1541},[65,9365,9366],{"class":1389}," c.env.",[65,9368,9369],{"class":1574},"JWT_SECRET",[65,9371,9372],{"class":1389}," }));\n",[65,9374,9375],{"class":67,"line":1196},[65,9376,279],{"emptyLinePlaceholder":278},[65,9378,9379,9381,9384,9386,9389,9391,9393,9395,9397,9399,9401],{"class":67,"line":1201},[65,9380,9314],{"class":1389},[65,9382,9383],{"class":1534},"get",[65,9385,1783],{"class":1389},[65,9387,9388],{"class":1410},"\"/api/products\"",[65,9390,2900],{"class":1389},[65,9392,2880],{"class":1541},[65,9394,538],{"class":1389},[65,9396,9359],{"class":1791},[65,9398,1795],{"class":1389},[65,9400,1798],{"class":1541},[65,9402,1801],{"class":1389},[65,9404,9405,9407,9410,9412,9414,9416,9419,9421,9423,9425,9428,9430,9433],{"class":67,"line":1207},[65,9406,1932],{"class":1541},[65,9408,9409],{"class":1574}," cached",[65,9411,1741],{"class":1541},[65,9413,1900],{"class":1541},[65,9415,9366],{"class":1389},[65,9417,9418],{"class":1574},"CACHE",[65,9420,758],{"class":1389},[65,9422,9383],{"class":1534},[65,9424,1783],{"class":1389},[65,9426,9427],{"class":1410},"\"products:all\"",[65,9429,2900],{"class":1389},[65,9431,9432],{"class":1410},"\"json\"",[65,9434,9435],{"class":1389},");\n",[65,9437,9438,9440],{"class":67,"line":1213},[65,9439,1806],{"class":1541},[65,9441,9442],{"class":1389}," (cached) {\n",[65,9444,9445,9447,9450,9452],{"class":67,"line":1219},[65,9446,5127],{"class":1541},[65,9448,9449],{"class":1389}," c.",[65,9451,3884],{"class":1534},[65,9453,9454],{"class":1389},"(cached);\n",[65,9456,9457],{"class":67,"line":3094},[65,9458,1862],{"class":1389},[65,9460,9461],{"class":67,"line":3099},[65,9462,279],{"emptyLinePlaceholder":278},[65,9464,9465,9467,9469,9472,9475,9477,9479,9481,9484,9486,9489],{"class":67,"line":3104},[65,9466,1932],{"class":1541},[65,9468,4692],{"class":1389},[65,9470,9471],{"class":1574},"results",[65,9473,9474],{"class":1389}," } ",[65,9476,2937],{"class":1541},[65,9478,1900],{"class":1541},[65,9480,9366],{"class":1389},[65,9482,9483],{"class":1574},"DB",[65,9485,758],{"class":1389},[65,9487,9488],{"class":1534},"prepare",[65,9490,2764],{"class":1389},[65,9492,9493],{"class":67,"line":3122},[65,9494,9495],{"class":1410}," \"SELECT id, name, price FROM products WHERE active = 1\"\n",[65,9497,9498,9501,9504],{"class":67,"line":7237},[65,9499,9500],{"class":1389}," ).",[65,9502,9503],{"class":1534},"all",[65,9505,9506],{"class":1389},"();\n",[65,9508,9509],{"class":67,"line":7247},[65,9510,279],{"emptyLinePlaceholder":278},[65,9512,9513,9515,9517,9519,9521,9524,9526,9528,9530,9533,9535,9538],{"class":67,"line":7257},[65,9514,1900],{"class":1541},[65,9516,9366],{"class":1389},[65,9518,9418],{"class":1574},[65,9520,758],{"class":1389},[65,9522,9523],{"class":1534},"put",[65,9525,1783],{"class":1389},[65,9527,9427],{"class":1410},[65,9529,2900],{"class":1389},[65,9531,9532],{"class":1574},"JSON",[65,9534,758],{"class":1389},[65,9536,9537],{"class":1534},"stringify",[65,9539,9540],{"class":1389},"(results), {\n",[65,9542,9543,9546,9549],{"class":67,"line":7268},[65,9544,9545],{"class":1389}," expirationTtl: ",[65,9547,9548],{"class":1574},"300",[65,9550,3909],{"class":1389},[65,9552,9553],{"class":67,"line":7273},[65,9554,9555],{"class":1389}," });\n",[65,9557,9558],{"class":67,"line":7280},[65,9559,279],{"emptyLinePlaceholder":278},[65,9561,9562,9564,9566,9568],{"class":67,"line":7290},[65,9563,5127],{"class":1541},[65,9565,9449],{"class":1389},[65,9567,3884],{"class":1534},[65,9569,9570],{"class":1389},"(results);\n",[65,9572,9573],{"class":67,"line":7295},[65,9574,9575],{"class":1389},"});\n",[65,9577,9578],{"class":67,"line":7303},[65,9579,279],{"emptyLinePlaceholder":278},[65,9581,9583,9585,9587],{"class":67,"line":9582},33,[65,9584,8114],{"class":1389},[65,9586,8359],{"class":1541},[65,9588,9589],{"class":1389}," app;\n",[15,9591,9592],{},"This gives you routing, CORS, JWT authentication, KV caching, and D1 database access in under 30 lines. The entire thing deploys to 300-plus locations in about 15 seconds.",[22,9594,9596],{"id":9595},"code-examples-solving-real-problems-at-the-edge","Code Examples: Solving Real Problems at the Edge",[938,9598,9600],{"id":9599},"geolocation-based-routing","Geolocation-Based Routing",[15,9602,9603,9604,9607],{},"Cloudflare provides ",[33,9605,9606],{},"cf"," properties on every request with detailed geolocation data. No third-party API needed.",[56,9609,9611],{"className":1726,"code":9610,"language":1728,"meta":61,"style":61},"export default {\n async fetch(request: Request): Promise\u003CResponse> {\n const country = request.cf?.country as string;\n const region = request.cf?.region as string;\n\n // Enforce GDPR compliance — EU users hit EU-hosted API\n const euCountries = new Set([\n \"DE\", \"FR\", \"IT\", \"ES\", \"NL\", \"BE\", \"AT\", \"PL\",\n \"SE\", \"DK\", \"FI\", \"IE\", \"PT\", \"GR\", \"CZ\", \"RO\",\n ]);\n\n const apiBase = euCountries.has(country)\n ? \"https://api-eu.example.com\"\n : \"https://api-us.example.com\";\n\n const url = new URL(request.url);\n const apiUrl = `${apiBase}${url.pathname}${url.search}`;\n\n return fetch(apiUrl, {\n method: request.method,\n headers: request.headers,\n body: request.body,\n });\n },\n};\n",[33,9612,9613,9622,9653,9672,9690,9694,9699,9716,9758,9800,9805,9809,9827,9835,9845,9849,9866,9905,9909,9918,9923,9928,9933,9937,9941],{"__ignoreMap":61},[65,9614,9615,9618,9620],{"class":67,"line":68},[65,9616,9617],{"class":1541},"export",[65,9619,4908],{"class":1541},[65,9621,1801],{"class":1389},[65,9623,9624,9626,9629,9631,9634,9636,9639,9641,9643,9645,9647,9650],{"class":67,"line":74},[65,9625,2769],{"class":1541},[65,9627,9628],{"class":1534}," fetch",[65,9630,1783],{"class":1389},[65,9632,9633],{"class":1791},"request",[65,9635,2894],{"class":1541},[65,9637,9638],{"class":1534}," Request",[65,9640,876],{"class":1389},[65,9642,2894],{"class":1541},[65,9644,4615],{"class":1534},[65,9646,2946],{"class":1389},[65,9648,9649],{"class":1534},"Response",[65,9651,9652],{"class":1389},"> {\n",[65,9654,9655,9657,9660,9662,9665,9668,9670],{"class":67,"line":80},[65,9656,1932],{"class":1541},[65,9658,9659],{"class":1574}," country",[65,9661,1741],{"class":1541},[65,9663,9664],{"class":1389}," request.cf?.country ",[65,9666,9667],{"class":1541},"as",[65,9669,2897],{"class":1574},[65,9671,9208],{"class":1389},[65,9673,9674,9676,9679,9681,9684,9686,9688],{"class":67,"line":86},[65,9675,1932],{"class":1541},[65,9677,9678],{"class":1574}," region",[65,9680,1741],{"class":1541},[65,9682,9683],{"class":1389}," request.cf?.region ",[65,9685,9667],{"class":1541},[65,9687,2897],{"class":1574},[65,9689,9208],{"class":1389},[65,9691,9692],{"class":67,"line":92},[65,9693,279],{"emptyLinePlaceholder":278},[65,9695,9696],{"class":67,"line":98},[65,9697,9698],{"class":1379}," // Enforce GDPR compliance — EU users hit EU-hosted API\n",[65,9700,9701,9703,9706,9708,9710,9713],{"class":67,"line":104},[65,9702,1932],{"class":1541},[65,9704,9705],{"class":1574}," euCountries",[65,9707,1741],{"class":1541},[65,9709,1744],{"class":1541},[65,9711,9712],{"class":1534}," Set",[65,9714,9715],{"class":1389},"([\n",[65,9717,9718,9721,9723,9726,9728,9731,9733,9736,9738,9741,9743,9746,9748,9751,9753,9756],{"class":67,"line":110},[65,9719,9720],{"class":1410}," \"DE\"",[65,9722,2900],{"class":1389},[65,9724,9725],{"class":1410},"\"FR\"",[65,9727,2900],{"class":1389},[65,9729,9730],{"class":1410},"\"IT\"",[65,9732,2900],{"class":1389},[65,9734,9735],{"class":1410},"\"ES\"",[65,9737,2900],{"class":1389},[65,9739,9740],{"class":1410},"\"NL\"",[65,9742,2900],{"class":1389},[65,9744,9745],{"class":1410},"\"BE\"",[65,9747,2900],{"class":1389},[65,9749,9750],{"class":1410},"\"AT\"",[65,9752,2900],{"class":1389},[65,9754,9755],{"class":1410},"\"PL\"",[65,9757,3909],{"class":1389},[65,9759,9760,9763,9765,9768,9770,9773,9775,9778,9780,9783,9785,9788,9790,9793,9795,9798],{"class":67,"line":367},[65,9761,9762],{"class":1410}," \"SE\"",[65,9764,2900],{"class":1389},[65,9766,9767],{"class":1410},"\"DK\"",[65,9769,2900],{"class":1389},[65,9771,9772],{"class":1410},"\"FI\"",[65,9774,2900],{"class":1389},[65,9776,9777],{"class":1410},"\"IE\"",[65,9779,2900],{"class":1389},[65,9781,9782],{"class":1410},"\"PT\"",[65,9784,2900],{"class":1389},[65,9786,9787],{"class":1410},"\"GR\"",[65,9789,2900],{"class":1389},[65,9791,9792],{"class":1410},"\"CZ\"",[65,9794,2900],{"class":1389},[65,9796,9797],{"class":1410},"\"RO\"",[65,9799,3909],{"class":1389},[65,9801,9802],{"class":67,"line":431},[65,9803,9804],{"class":1389}," ]);\n",[65,9806,9807],{"class":67,"line":713},[65,9808,279],{"emptyLinePlaceholder":278},[65,9810,9811,9813,9816,9818,9821,9824],{"class":67,"line":1043},[65,9812,1932],{"class":1541},[65,9814,9815],{"class":1574}," apiBase",[65,9817,1741],{"class":1541},[65,9819,9820],{"class":1389}," euCountries.",[65,9822,9823],{"class":1534},"has",[65,9825,9826],{"class":1389},"(country)\n",[65,9828,9829,9832],{"class":67,"line":1049},[65,9830,9831],{"class":1541}," ?",[65,9833,9834],{"class":1410}," \"https://api-eu.example.com\"\n",[65,9836,9837,9840,9843],{"class":67,"line":1190},[65,9838,9839],{"class":1541}," :",[65,9841,9842],{"class":1410}," \"https://api-us.example.com\"",[65,9844,9208],{"class":1389},[65,9846,9847],{"class":67,"line":1196},[65,9848,279],{"emptyLinePlaceholder":278},[65,9850,9851,9853,9856,9858,9860,9863],{"class":67,"line":1201},[65,9852,1932],{"class":1541},[65,9854,9855],{"class":1574}," url",[65,9857,1741],{"class":1541},[65,9859,1744],{"class":1541},[65,9861,9862],{"class":1534}," URL",[65,9864,9865],{"class":1389},"(request.url);\n",[65,9867,9868,9870,9873,9875,9878,9881,9884,9887,9889,9892,9894,9896,9898,9901,9903],{"class":67,"line":1207},[65,9869,1932],{"class":1541},[65,9871,9872],{"class":1574}," apiUrl",[65,9874,1741],{"class":1541},[65,9876,9877],{"class":1410}," `${",[65,9879,9880],{"class":1389},"apiBase",[65,9882,9883],{"class":1410},"}${",[65,9885,9886],{"class":1389},"url",[65,9888,758],{"class":1410},[65,9890,9891],{"class":1389},"pathname",[65,9893,9883],{"class":1410},[65,9895,9886],{"class":1389},[65,9897,758],{"class":1410},[65,9899,9900],{"class":1389},"search",[65,9902,1854],{"class":1410},[65,9904,9208],{"class":1389},[65,9906,9907],{"class":67,"line":1213},[65,9908,279],{"emptyLinePlaceholder":278},[65,9910,9911,9913,9915],{"class":67,"line":1219},[65,9912,5127],{"class":1541},[65,9914,9628],{"class":1534},[65,9916,9917],{"class":1389},"(apiUrl, {\n",[65,9919,9920],{"class":67,"line":3094},[65,9921,9922],{"class":1389}," method: request.method,\n",[65,9924,9925],{"class":67,"line":3099},[65,9926,9927],{"class":1389}," headers: request.headers,\n",[65,9929,9930],{"class":67,"line":3104},[65,9931,9932],{"class":1389}," body: request.body,\n",[65,9934,9935],{"class":67,"line":3122},[65,9936,9555],{"class":1389},[65,9938,9939],{"class":67,"line":7237},[65,9940,2790],{"class":1389},[65,9942,9943],{"class":67,"line":7247},[65,9944,9275],{"class":1389},[938,9946,9948],{"id":9947},"edge-middleware-rate-limiting-with-kv","Edge Middleware: Rate Limiting with KV",[56,9950,9952],{"className":1726,"code":9951,"language":1728,"meta":61,"style":61},"type Env = {\n RATE_LIMIT: KVNamespace;\n};\n\nConst WINDOW_MS = 60_000;\nconst MAX_REQUESTS = 100;\n\nExport default {\n async fetch(request: Request, env: Env): Promise\u003CResponse> {\n const ip = request.headers.get(\"cf-connecting-ip\") ?? \"unknown\";\n const key = `rate:${ip}`;\n\n const current = await env.RATE_LIMIT.get(key, \"json\") as {\n count: number;\n resetAt: number;\n } | null;\n\n const now = Date.now();\n\n if (current && now \u003C current.resetAt) {\n if (current.count >= MAX_REQUESTS) {\n return new Response(\"Too Many Requests\", {\n status: 429,\n headers: {\n \"Retry-After\": String(Math.ceil((current.resetAt - now) / 1000)),\n },\n });\n }\n await env.RATE_LIMIT.put(\n key,\n JSON.stringify({ count: current.count + 1, resetAt: current.resetAt }),\n { expirationTtl: 120 }\n );\n } else {\n await env.RATE_LIMIT.put(\n key,\n JSON.stringify({ count: 1, resetAt: now + WINDOW_MS }),\n { expirationTtl: 120 }\n );\n }\n\n // Pass through to origin\n return fetch(request);\n },\n};\n",[33,9953,9954,9966,9978,9982,9986,10001,10014,10018,10026,10046,10073,10090,10094,10124,10135,10146,10156,10160,10175,10179,10196,10210,10226,10236,10241,10273,10277,10281,10285,10299,10304,10323,10333,10338,10344,10359,10364,10389,10398,10403,10408,10413,10419,10433,10438],{"__ignoreMap":61},[65,9955,9956,9959,9962,9964],{"class":67,"line":68},[65,9957,9958],{"class":1541},"type",[65,9960,9961],{"class":1534}," Env",[65,9963,1741],{"class":1541},[65,9965,1801],{"class":1389},[65,9967,9968,9971,9973,9976],{"class":67,"line":74},[65,9969,9970],{"class":1791}," RATE_LIMIT",[65,9972,2894],{"class":1541},[65,9974,9975],{"class":1534}," KVNamespace",[65,9977,9208],{"class":1389},[65,9979,9980],{"class":67,"line":80},[65,9981,9275],{"class":1389},[65,9983,9984],{"class":67,"line":86},[65,9985,279],{"emptyLinePlaceholder":278},[65,9987,9988,9991,9994,9996,9999],{"class":67,"line":92},[65,9989,9990],{"class":1389},"Const ",[65,9992,9993],{"class":1574},"WINDOW_MS",[65,9995,1741],{"class":1541},[65,9997,9998],{"class":1574}," 60_000",[65,10000,9208],{"class":1389},[65,10002,10003,10005,10008,10010,10012],{"class":67,"line":98},[65,10004,1735],{"class":1541},[65,10006,10007],{"class":1574}," MAX_REQUESTS",[65,10009,1741],{"class":1541},[65,10011,1815],{"class":1574},[65,10013,9208],{"class":1389},[65,10015,10016],{"class":67,"line":104},[65,10017,279],{"emptyLinePlaceholder":278},[65,10019,10020,10022,10024],{"class":67,"line":110},[65,10021,8114],{"class":1389},[65,10023,8359],{"class":1541},[65,10025,1801],{"class":1389},[65,10027,10028,10030,10033,10036,10038,10040,10042,10044],{"class":67,"line":367},[65,10029,4739],{"class":1389},[65,10031,10032],{"class":1534},"fetch",[65,10034,10035],{"class":1389},"(request: Request, env: Env): ",[65,10037,4748],{"class":1574},[65,10039,2946],{"class":1541},[65,10041,9649],{"class":1389},[65,10043,1812],{"class":1541},[65,10045,1801],{"class":1389},[65,10047,10048,10051,10053,10056,10058,10060,10063,10065,10068,10071],{"class":67,"line":431},[65,10049,10050],{"class":1389}," const ip ",[65,10052,2937],{"class":1541},[65,10054,10055],{"class":1389}," request.headers.",[65,10057,9383],{"class":1534},[65,10059,1783],{"class":1389},[65,10061,10062],{"class":1410},"\"cf-connecting-ip\"",[65,10064,1795],{"class":1389},[65,10066,10067],{"class":1541},"??",[65,10069,10070],{"class":1410}," \"unknown\"",[65,10072,9208],{"class":1389},[65,10074,10075,10078,10080,10083,10086,10088],{"class":67,"line":713},[65,10076,10077],{"class":1389}," const key ",[65,10079,2937],{"class":1541},[65,10081,10082],{"class":1410}," `rate:${",[65,10084,10085],{"class":1389},"ip",[65,10087,1854],{"class":1410},[65,10089,9208],{"class":1389},[65,10091,10092],{"class":67,"line":1043},[65,10093,279],{"emptyLinePlaceholder":278},[65,10095,10096,10099,10101,10103,10106,10109,10111,10113,10116,10118,10120,10122],{"class":67,"line":1049},[65,10097,10098],{"class":1389}," const current ",[65,10100,2937],{"class":1541},[65,10102,1900],{"class":1541},[65,10104,10105],{"class":1389}," env.",[65,10107,10108],{"class":1574},"RATE_LIMIT",[65,10110,758],{"class":1389},[65,10112,9383],{"class":1534},[65,10114,10115],{"class":1389},"(key, ",[65,10117,9432],{"class":1410},[65,10119,1795],{"class":1389},[65,10121,9667],{"class":1541},[65,10123,1801],{"class":1389},[65,10125,10126,10129,10131,10133],{"class":67,"line":1190},[65,10127,10128],{"class":1791}," count",[65,10130,2894],{"class":1541},[65,10132,2908],{"class":1574},[65,10134,9208],{"class":1389},[65,10136,10137,10140,10142,10144],{"class":67,"line":1196},[65,10138,10139],{"class":1791}," resetAt",[65,10141,2894],{"class":1541},[65,10143,2908],{"class":1574},[65,10145,9208],{"class":1389},[65,10147,10148,10150,10152,10154],{"class":67,"line":1201},[65,10149,9474],{"class":1389},[65,10151,1542],{"class":1541},[65,10153,5269],{"class":1574},[65,10155,9208],{"class":1389},[65,10157,10158],{"class":67,"line":1207},[65,10159,279],{"emptyLinePlaceholder":278},[65,10161,10162,10165,10167,10170,10173],{"class":67,"line":1213},[65,10163,10164],{"class":1389}," const now ",[65,10166,2937],{"class":1541},[65,10168,10169],{"class":1389}," Date.",[65,10171,10172],{"class":1534},"now",[65,10174,9506],{"class":1389},[65,10176,10177],{"class":67,"line":1219},[65,10178,279],{"emptyLinePlaceholder":278},[65,10180,10181,10183,10185,10188,10191,10193],{"class":67,"line":3094},[65,10182,1806],{"class":1534},[65,10184,538],{"class":1389},[65,10186,10187],{"class":1791},"current",[65,10189,10190],{"class":1389}," && ",[65,10192,10172],{"class":1791},[65,10194,10195],{"class":1389}," \u003C current.resetAt) {\n",[65,10197,10198,10200,10203,10206,10208],{"class":67,"line":3099},[65,10199,1806],{"class":1541},[65,10201,10202],{"class":1389}," (current.count ",[65,10204,10205],{"class":1541},">=",[65,10207,10007],{"class":1574},[65,10209,2921],{"class":1389},[65,10211,10212,10214,10216,10219,10221,10224],{"class":67,"line":3104},[65,10213,5127],{"class":1541},[65,10215,1744],{"class":1541},[65,10217,10218],{"class":1534}," Response",[65,10220,1783],{"class":1389},[65,10222,10223],{"class":1410},"\"Too Many Requests\"",[65,10225,8131],{"class":1389},[65,10227,10228,10231,10234],{"class":67,"line":3122},[65,10229,10230],{"class":1389}," status: ",[65,10232,10233],{"class":1574},"429",[65,10235,3909],{"class":1389},[65,10237,10238],{"class":67,"line":7237},[65,10239,10240],{"class":1389}," headers: {\n",[65,10242,10243,10246,10248,10251,10254,10257,10260,10262,10265,10267,10270],{"class":67,"line":7247},[65,10244,10245],{"class":1410}," \"Retry-After\"",[65,10247,1407],{"class":1389},[65,10249,10250],{"class":1534},"String",[65,10252,10253],{"class":1389},"(Math.",[65,10255,10256],{"class":1534},"ceil",[65,10258,10259],{"class":1389},"((current.resetAt ",[65,10261,3314],{"class":1541},[65,10263,10264],{"class":1389}," now) ",[65,10266,2336],{"class":1541},[65,10268,10269],{"class":1574}," 1000",[65,10271,10272],{"class":1389},")),\n",[65,10274,10275],{"class":67,"line":7257},[65,10276,2790],{"class":1389},[65,10278,10279],{"class":67,"line":7268},[65,10280,9555],{"class":1389},[65,10282,10283],{"class":67,"line":7273},[65,10284,1862],{"class":1389},[65,10286,10287,10289,10291,10293,10295,10297],{"class":67,"line":7280},[65,10288,1900],{"class":1541},[65,10290,10105],{"class":1389},[65,10292,10108],{"class":1574},[65,10294,758],{"class":1389},[65,10296,9523],{"class":1534},[65,10298,2764],{"class":1389},[65,10300,10301],{"class":67,"line":7290},[65,10302,10303],{"class":1389}," key,\n",[65,10305,10306,10309,10311,10313,10316,10318,10320],{"class":67,"line":7295},[65,10307,10308],{"class":1574}," JSON",[65,10310,758],{"class":1389},[65,10312,9537],{"class":1534},[65,10314,10315],{"class":1389},"({ count: current.count ",[65,10317,3040],{"class":1541},[65,10319,3078],{"class":1574},[65,10321,10322],{"class":1389},", resetAt: current.resetAt }),\n",[65,10324,10325,10328,10331],{"class":67,"line":7303},[65,10326,10327],{"class":1389}," { expirationTtl: ",[65,10329,10330],{"class":1574},"120",[65,10332,1862],{"class":1389},[65,10334,10335],{"class":67,"line":9582},[65,10336,10337],{"class":1389}," );\n",[65,10339,10341],{"class":67,"line":10340},34,[65,10342,10343],{"class":1389}," } else {\n",[65,10345,10347,10349,10351,10353,10355,10357],{"class":67,"line":10346},35,[65,10348,1900],{"class":1541},[65,10350,10105],{"class":1389},[65,10352,10108],{"class":1574},[65,10354,758],{"class":1389},[65,10356,9523],{"class":1534},[65,10358,2764],{"class":1389},[65,10360,10362],{"class":67,"line":10361},36,[65,10363,10303],{"class":1389},[65,10365,10367,10369,10371,10373,10376,10378,10381,10383,10386],{"class":67,"line":10366},37,[65,10368,10308],{"class":1574},[65,10370,758],{"class":1389},[65,10372,9537],{"class":1534},[65,10374,10375],{"class":1389},"({ count: ",[65,10377,3051],{"class":1574},[65,10379,10380],{"class":1389},", resetAt: now ",[65,10382,3040],{"class":1541},[65,10384,10385],{"class":1574}," WINDOW_MS",[65,10387,10388],{"class":1389}," }),\n",[65,10390,10392,10394,10396],{"class":67,"line":10391},38,[65,10393,10327],{"class":1389},[65,10395,10330],{"class":1574},[65,10397,1862],{"class":1389},[65,10399,10401],{"class":67,"line":10400},39,[65,10402,10337],{"class":1389},[65,10404,10406],{"class":67,"line":10405},40,[65,10407,1862],{"class":1389},[65,10409,10411],{"class":67,"line":10410},41,[65,10412,279],{"emptyLinePlaceholder":278},[65,10414,10416],{"class":67,"line":10415},42,[65,10417,10418],{"class":1379}," // Pass through to origin\n",[65,10420,10422,10425,10427,10429,10431],{"class":67,"line":10421},43,[65,10423,10424],{"class":1389}," return ",[65,10426,10032],{"class":1534},[65,10428,1783],{"class":1389},[65,10430,9633],{"class":1791},[65,10432,9435],{"class":1389},[65,10434,10436],{"class":67,"line":10435},44,[65,10437,2790],{"class":1389},[65,10439,10441],{"class":67,"line":10440},45,[65,10442,9275],{"class":1389},[15,10444,10445],{},"This is approximate rate limiting — KV is eventually consistent, so a burst of simultaneous requests might slip past the limit briefly. For most applications, that is acceptable. For financial-grade rate limiting, you would want Durable Objects instead.",[22,10447,10449],{"id":10448},"workers-vs-traditional-serverless","Workers vs Traditional Serverless",[15,10451,10452],{},"I have run workloads on Lambda, Cloud Functions, and Workers. Here is my honest comparison.",[15,10454,10455,10458],{},[124,10456,10457],{},"Cold starts."," Lambda cold starts range from 100ms to several seconds depending on runtime and bundle size. Workers cold starts are effectively zero — V8 isolates spin up in under a millisecond. For user-facing endpoints, this difference is night and day.",[15,10460,10461,10464],{},[124,10462,10463],{},"Global distribution."," Lambda runs in whichever region you deploy to. Multi-region Lambda requires explicit configuration, multiple deployments, and usually DynamoDB global tables. Workers deploy globally by default. There is no configuration step for \"make this available worldwide.\"",[15,10466,10467,10470],{},[124,10468,10469],{},"Runtime environment."," Lambda gives you a full Node.js (or Python, Go, etc.) runtime. Workers give you a stripped-down V8 isolate. If your code depends on Node.js APIs, native modules, or heavy server-side libraries, Workers will fight you. Lambda will not.",[15,10472,10473,10476,10477,758],{},[124,10474,10475],{},"Cost."," Workers' free tier covers 100,000 requests per day. Paid plans start at $5/month for 10 million requests. Lambda pricing is more complex — you pay for invocations, duration, and memory — but for equivalent workloads, Workers tend to be cheaper. I covered broader cost strategies in my ",[752,10478,10480],{"href":10479},"/blog/cloud-cost-optimization","cloud cost optimization guide",[15,10482,10483,10486],{},[124,10484,10485],{},"Execution limits."," Lambda allows up to 15 minutes of execution time and 10GB of memory. Workers cap at 30 seconds on the paid plan (10ms CPU on free) and 128MB of memory. These are fundamentally different constraint profiles.",[22,10488,10490],{"id":10489},"the-limitations-nobody-talks-about","The Limitations Nobody Talks About",[938,10492,10494],{"id":10493},"no-long-running-processes","No Long-Running Processes",[15,10496,10497],{},"If your operation takes more than 30 seconds, Workers is the wrong tool. Background jobs, video processing, complex data pipelines — these belong on traditional compute. Workers are designed for request-response cycles, not batch processing.",[938,10499,10501],{"id":10500},"_128mb-memory-ceiling","128MB Memory Ceiling",[15,10503,10504],{},"You cannot load a large ML model, process a massive CSV, or hold a significant dataset in memory. Workers are for lightweight, focused operations. If you are doing heavy computation, you need a server.",[938,10506,10508],{"id":10507},"eventually-consistent-storage","Eventually Consistent Storage",[15,10510,10511],{},"KV reads are fast globally. KV writes take up to 60 seconds to propagate. If your application requires strong consistency — \"write then immediately read back the same value from another region\" — KV alone will not satisfy that requirement. D1 provides stronger consistency but is a single-region database under the hood, which partially defeats the purpose of edge distribution.",[938,10513,10515],{"id":10514},"the-v8-isolate-environment","The V8 Isolate Environment",[15,10517,10518,10519,2900,10522,10525,10526,10529],{},"Workers do not run Node.js. They run in V8 isolates with Web API compatibility. Many npm packages work fine. Some do not — anything touching ",[33,10520,10521],{},"fs",[33,10523,10524],{},"child_process",", Node-specific ",[33,10527,10528],{},"crypto",", or native C++ bindings will fail. You will spend time finding Workers-compatible alternatives or restructuring code. Libraries have gotten much better about this, but it remains a real friction point.",[22,10531,10533],{"id":10532},"when-not-to-put-things-at-the-edge","When NOT to Put Things at the Edge",[15,10535,10536],{},"This is the part most Cloudflare tutorials skip. Edge computing is not universally better. Here are cases where a centralized server is the right call.",[15,10538,10539,10542,10543,10547],{},[124,10540,10541],{},"Database-heavy operations."," If every request requires multiple database queries, and your database lives in a single region, running your code at the edge just adds a network hop back to the database. You have moved your compute farther from your data. The user sees higher latency, not lower. This is the most common mistake I see. The techniques I covered in ",[752,10544,10546],{"href":10545},"/blog/api-performance-optimization","API performance optimization"," matter more than where your code runs if your bottleneck is database round trips.",[15,10549,10550,10553],{},[124,10551,10552],{},"Complex business logic."," Multi-step transactions, workflow orchestration, operations involving multiple services — these benefit from co-location with your data layer and other services, not from geographic distribution.",[15,10555,10556,10559],{},[124,10557,10558],{},"Large dependencies."," If your application needs heavy libraries — image processing, PDF generation, ML inference — the 128MB memory limit and the absence of native modules make Workers impractical. Use a proper server.",[15,10561,10562,10565],{},[124,10563,10564],{},"Development velocity."," Workers have a different debugging model, different runtime constraints, and a smaller ecosystem. If your team is not familiar with the platform, the learning curve costs development speed. Sometimes the 200ms round trip to a traditional server is a perfectly acceptable trade-off for faster iteration.",[22,10567,10569],{"id":10568},"the-decision-framework","The Decision Framework",[15,10571,10572],{},"My mental model is straightforward. If the operation is stateless or reads from eventually consistent storage, runs in under 50ms of CPU time, and serves users globally — put it at the edge. If it requires strong consistency, complex transactions, or heavy compute — keep it centralized.",[15,10574,10575],{},"The edge is not a replacement for your server. It is a layer in front of it that handles the work that benefits from geographic proximity. Get that boundary right and you get the performance gains without fighting the platform.",[746,10577],{},[22,10579,764],{"id":763},[118,10581,10582,10587,10592,10598],{},[121,10583,10584],{},[752,10585,10586],{"href":9173},"Cloudflare Pages: The Fastest Way to Deploy Your Frontend",[121,10588,10589],{},[752,10590,10591],{"href":10545},"API Performance Optimization: Making Your Endpoints Fast at Scale",[121,10593,10594],{},[752,10595,10597],{"href":10596},"/blog/cdn-configuration-guide","CDN Configuration: Making Your Static Assets Load Instantly Everywhere",[121,10599,10600],{},[752,10601,10602],{"href":10479},"Cloud Cost Optimization: Cutting the Bill Without Cutting Corners",[792,10604,10605],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":80,"depth":80,"links":10607},[10608,10609,10615,10616,10620,10621,10627,10628,10629],{"id":9125,"depth":74,"text":9126},{"id":9138,"depth":74,"text":9139,"children":10610},[10611,10612,10613,10614],{"id":9145,"depth":80,"text":9146},{"id":9152,"depth":80,"text":9153},{"id":9159,"depth":80,"text":9160},{"id":9166,"depth":80,"text":9167},{"id":9178,"depth":74,"text":9179},{"id":9595,"depth":74,"text":9596,"children":10617},[10618,10619],{"id":9599,"depth":80,"text":9600},{"id":9947,"depth":80,"text":9948},{"id":10448,"depth":74,"text":10449},{"id":10489,"depth":74,"text":10490,"children":10622},[10623,10624,10625,10626],{"id":10493,"depth":80,"text":10494},{"id":10500,"depth":80,"text":10501},{"id":10507,"depth":80,"text":10508},{"id":10514,"depth":80,"text":10515},{"id":10532,"depth":74,"text":10533},{"id":10568,"depth":74,"text":10569},{"id":763,"depth":74,"text":764},"A developer's guide to Cloudflare Workers and edge computing — when to move logic to the edge, how to structure Worker code, and the trade-offs that most tutorials skip.",[10632,10633,10634,10635,10636],"cloudflare workers guide","edge computing architecture","cloudflare workers typescript","edge functions vs serverless","cloudflare workers tutorial",{},"/blog/edge-computing-cloudflare-workers",{"title":9110,"description":10630},"blog/edge-computing-cloudflare-workers",[10642,10643,824,10644,10645],"Edge Computing","Cloudflare Workers","Cloud Infrastructure","Serverless","SfdF9yP26u7G_k0VshN-LAc0KnkH6opNEri9n4SYwMI",{"id":10648,"title":10649,"author":10650,"body":10651,"category":808,"date":809,"description":10931,"extension":811,"featured":812,"image":813,"keywords":10932,"meta":10935,"navigation":278,"path":10936,"readTime":431,"seo":10937,"stem":10938,"tags":10939,"__hash__":10943},"blog/blog/enterprise-data-management.md","Enterprise Data Management: Building the Single Source of Truth",{"name":9,"bio":10},{"type":12,"value":10652,"toc":10921},[10653,10657,10660,10663,10666,10670,10673,10676,10679,10682,10687,10710,10713,10717,10720,10726,10729,10732,10735,10738,10743,10746,10749,10753,10756,10762,10768,10774,10780,10783,10787,10790,10804,10807,10813,10819,10825,10831,10835,10838,10841,10844,10870,10873,10877,10880,10883,10886,10893,10895,10897],[22,10654,10656],{"id":10655},"the-data-that-nobody-trusts","The Data That Nobody Trusts",[15,10658,10659],{},"A company has three software systems. The CRM says they have 847 active customers. The billing system says 912. The customer success platform says 803. A board presentation is coming up. The CEO asks for the customer count. The data team spends two days reconciling the numbers and produces 871 with a footnote explaining the methodology.",[15,10661,10662],{},"Every organization above a certain size has this problem. It's not a technology problem — it's a data architecture problem. Specifically, it's the absence of a deliberate answer to the question: which system is the authoritative source for each category of data?",[15,10664,10665],{},"This is what enterprise data management is actually about: not data warehouses or ETL pipelines (those are implementation details) but the design decisions that determine which data is trusted, who owns it, and how it flows through the organization.",[22,10667,10669],{"id":10668},"the-concept-of-data-domains-and-ownership","The Concept of Data Domains and Ownership",[15,10671,10672],{},"The foundation of good enterprise data management is domain ownership: for each major category of data, exactly one system is authoritative and one team owns the quality of that data.",[15,10674,10675],{},"This seems simple but requires organizational decisions most companies avoid. When you say \"the CRM owns the customer record,\" you're also saying the ERP, the billing system, and the customer success platform get their customer data from the CRM — they don't maintain their own. You're saying the sales operations team is responsible for the quality of customer data. You're saying that when systems disagree, the CRM wins.",[15,10677,10678],{},"These are politically difficult decisions. Different teams have emotional and practical stakes in \"their\" data. The ERP team doesn't want to depend on the CRM team for customer records. The billing team has enriched their customer records with information the CRM doesn't have.",[15,10680,10681],{},"But without these decisions, every system maintains its own version of reality, and you're back to three numbers for customer count.",[15,10683,10684],{},[124,10685,10686],{},"Common data domains and typical ownership:",[118,10688,10689,10692,10695,10698,10701,10704,10707],{},[121,10690,10691],{},"Customer/account records: CRM",[121,10693,10694],{},"Product catalog: ERP or product information management (PIM) system",[121,10696,10697],{},"Financial transactions: ERP / accounting system",[121,10699,10700],{},"Employee records: HRIS",[121,10702,10703],{},"Inventory positions: ERP or warehouse management system",[121,10705,10706],{},"Orders: ERP or order management system",[121,10708,10709],{},"Interactions and relationship history: CRM",[15,10711,10712],{},"This is not universal — your business might have legitimate reasons to deviate. But having explicit answers, whatever they are, is the starting point.",[22,10714,10716],{"id":10715},"master-data-management-the-practice","Master Data Management: The Practice",[15,10718,10719],{},"Master data management (MDM) is the discipline of managing the data that represents your core business entities — customers, products, vendors, locations, employees. These are the records that appear across many systems and where consistency is most critical.",[15,10721,10722,10725],{},[124,10723,10724],{},"Customer MDM"," in practice:",[15,10727,10728],{},"The problem: you have customers in your CRM, in your billing system, in your support platform, in your marketing automation tool. These four systems have records for the same customers but with different IDs, different contact information (which is more current?), different segmentation, and some duplicates.",[15,10730,10731],{},"The solution: a master customer record that serves as the authoritative source. Every system that needs customer data reads from or syncs with the master record. The master record has a system-wide unique identifier that every other system uses to reference the customer.",[15,10733,10734],{},"Implementation options range from full-blown MDM platforms (Informatica, Reltio, Profisee) to a simpler approach: designate one system as master (usually the CRM) and build integrations that push customer data to downstream systems rather than having each system maintain its own.",[15,10736,10737],{},"The simpler approach is usually right for mid-market companies. Full MDM platforms are designed for enterprise scale and complexity — hundreds of systems, millions of customers, regulatory requirements around data quality. At smaller scale, they're over-engineered.",[15,10739,10740],{},[124,10741,10742],{},"Product master data:",[15,10744,10745],{},"Product information is often fragmented: specifications in engineering systems, pricing in the ERP, marketing descriptions in the website CMS, inventory codes in the WMS. A product information management system (PIM) centralizes this — or the ERP item master can serve as the single product definition if it's rich enough.",[15,10747,10748],{},"The critical thing is that product data doesn't diverge. The same product can't have different names, different codes, or different specifications in different systems. When it does, operational errors follow — wrong product shipped, mismatched inventory counts, incorrect pricing.",[22,10750,10752],{"id":10751},"data-integration-architecture","Data Integration Architecture",[15,10754,10755],{},"Once you've decided which system owns which data, you need to architect how data flows between systems. There are several patterns, each with tradeoffs.",[15,10757,10758,10761],{},[124,10759,10760],{},"Point-to-point integrations"," are the default that emerges without deliberate architecture. System A integrates directly with System B. System B integrates directly with System C. System C integrates directly with System A. Over time, you have a web of pairwise connections, each built differently, each managed separately. This is sometimes called \"spaghetti integration\" and it's the reason enterprise data management becomes unmanageable at scale.",[15,10763,10764,10767],{},[124,10765,10766],{},"Hub-and-spoke integration"," introduces a central integration hub. Instead of A integrating with B and C directly, A sends data to the hub, and the hub distributes to B and C. This centralizes integration management and makes adding new system connections easier — add a new spoke, not new point-to-point connections. The hub is also where data transformation happens, which centralizes that logic.",[15,10769,10770,10773],{},[124,10771,10772],{},"Event-driven integration"," treats data changes as events that are published to a message stream (Kafka, AWS EventBridge, Azure Service Bus). Systems that need the data subscribe to the relevant events and process them asynchronously. This decouples systems from each other — the customer-creating system doesn't need to know which downstream systems care about new customers. This is the most scalable and flexible integration architecture, but it requires more upfront design and infrastructure investment.",[15,10775,10776,10779],{},[124,10777,10778],{},"API-based integration"," where each system exposes APIs and consumes other systems' APIs directly. This is synchronous and tight-coupling by design. Appropriate for real-time lookups and transactional operations; inappropriate for bulk data sync or high-volume async processing.",[15,10781,10782],{},"Most enterprise environments end up using a combination: event-driven for asynchronous data propagation, APIs for real-time lookups, and some point-to-point where the complexity doesn't justify a hub.",[22,10784,10786],{"id":10785},"data-quality-as-an-operational-practice","Data Quality as an Operational Practice",[15,10788,10789],{},"Data quality doesn't maintain itself. Without active management, data quality degrades because:",[118,10791,10792,10795,10798,10801],{},[121,10793,10794],{},"People enter data inconsistently",[121,10796,10797],{},"Systems allow data that violates business rules",[121,10799,10800],{},"Integration transformations introduce errors",[121,10802,10803],{},"Records go stale as the real world changes",[15,10805,10806],{},"Data quality management is an operational practice, not a one-time cleanup project. It requires:",[15,10808,10809,10812],{},[124,10810,10811],{},"Validation at entry."," Data validation that enforces business rules at the point of entry — required fields, format constraints, referential integrity, business rule checks — prevents bad data from entering the system in the first place. This is cheaper than cleaning bad data after the fact by orders of magnitude.",[15,10814,10815,10818],{},[124,10816,10817],{},"Data quality monitoring."," Automated checks that measure data completeness, consistency, freshness, and accuracy on a schedule. Alerts when metrics fall below thresholds. A data quality dashboard that makes problems visible before they affect decisions.",[15,10820,10821,10824],{},[124,10822,10823],{},"Stewardship ownership."," Each data domain has a steward — a person or team responsible for data quality in that domain. The data steward doesn't do all the entry work, but they own the quality metrics, investigate anomalies, and are accountable for the data that downstream systems and decisions depend on.",[15,10826,10827,10830],{},[124,10828,10829],{},"Deduplication and merge workflows."," Duplicate records emerge despite best efforts — two salespeople create records for the same company with slightly different names, an integration creates a duplicate. Deduplication tools (machine learning-based matching, rule-based matching) identify likely duplicates for human review and merge. The workflow for this needs to be regular, not ad-hoc.",[22,10832,10834],{"id":10833},"the-data-warehouse-and-analytics-layer","The Data Warehouse and Analytics Layer",[15,10836,10837],{},"Once you have authoritative sources and clean integration, you can build reliable analytics.",[15,10839,10840],{},"The analytics layer is separate from the operational layer by design. Analytical queries (aggregations, historical trends, multi-system joins) should not run against operational databases — they compete for resources and can degrade application performance.",[15,10842,10843],{},"The modern analytics stack for mid-market companies:",[118,10845,10846,10852,10858,10864],{},[121,10847,10848,10851],{},[124,10849,10850],{},"ETL/ELT tool"," (Fivetran, Airbyte, or custom) to extract data from operational systems into the warehouse",[121,10853,10854,10857],{},[124,10855,10856],{},"Data warehouse"," (Snowflake, BigQuery, or Redshift for larger organizations; DuckDB or PostgreSQL for smaller scale)",[121,10859,10860,10863],{},[124,10861,10862],{},"Transformation layer"," (dbt) to define your metric logic as code — this is where your documented metric definitions become executable",[121,10865,10866,10869],{},[124,10867,10868],{},"BI tool"," (Tableau, Power BI, Metabase, or Looker) for dashboards and self-service analytics",[15,10871,10872],{},"The transformation layer is where data from multiple sources gets joined and shaped into your reporting models. The customer count discrepancy described at the opening of this article gets resolved here: the transformation model defines exactly what \"active customer\" means, pulls from the authoritative CRM source, and produces a single number that every report uses.",[22,10874,10876],{"id":10875},"where-to-start","Where to Start",[15,10878,10879],{},"Data management initiatives are easy to over-scope. The temptation is to tackle everything — all domains, all systems, full MDM platform — and then drown in complexity.",[15,10881,10882],{},"Start with the domain that causes the most business pain. If customer data discrepancies are causing the most problems, start there. Define ownership, fix the integration, establish quality monitoring for customer data. Get that working well before expanding scope.",[15,10884,10885],{},"This incremental approach produces demonstrable value quickly and builds organizational trust in the data management effort. That trust is what gives you the credibility to tackle the harder, more politically complex domains.",[15,10887,10888,10889,758],{},"If you're working through an enterprise data management initiative and want to talk through the architecture and prioritization, ",[752,10890,10892],{"href":754,"rel":10891},[756],"schedule a conversation at calendly.com/jamesrossjr",[746,10894],{},[22,10896,764],{"id":763},[118,10898,10899,10905,10911,10915],{},[121,10900,10901],{},[752,10902,10904],{"href":10903},"/blog/api-first-architecture","API-First Architecture: Building Software That Integrates by Default",[121,10906,10907],{},[752,10908,10910],{"href":10909},"/blog/enterprise-reporting-analytics","Enterprise Reporting and Analytics: Designing Systems That Tell the Truth",[121,10912,10913],{},[752,10914,6419],{"href":6418},[121,10916,10917],{},[752,10918,10920],{"href":10919},"/blog/enterprise-integration-patterns","Enterprise Integration Patterns That Actually Work in Production",{"title":61,"searchDepth":80,"depth":80,"links":10922},[10923,10924,10925,10926,10927,10928,10929,10930],{"id":10655,"depth":74,"text":10656},{"id":10668,"depth":74,"text":10669},{"id":10715,"depth":74,"text":10716},{"id":10751,"depth":74,"text":10752},{"id":10785,"depth":74,"text":10786},{"id":10833,"depth":74,"text":10834},{"id":10875,"depth":74,"text":10876},{"id":763,"depth":74,"text":764},"Without a deliberate data management strategy, every system becomes its own source of truth. Here's how to design enterprise data architecture that organizations can actually trust.",[10933,10934],"enterprise data management","data architecture",{},"/blog/enterprise-data-management",{"title":10649,"description":10931},"blog/enterprise-data-management",[10940,6458,10941,5622,10942],"Data Architecture","Data Management","Integration","0G13XyWxEE3_Ad1CbF9-GjoCdGgGtcQ7ljsq2zyhkEY",[10945,10947,10949,10951,10952,10954,10955,10956,10957,10958,10959,10960,10961,10962,10963,10964,10965,10966,10967,10968,10969,10970,10971,10972,10973,10974,10975,10976,10977,10978,10979,10980,10981,10982,10983,10984,10985,10986,10987,10988,10989,10990,10991,10992,10993,10994,10995,10996,10997,10998,10999,11000,11001,11002,11003,11004,11005,11006,11007,11008,11009,11010,11011,11012,11013,11014,11015,11016,11017,11018,11019,11020,11021,11022,11023,11024,11025,11026,11027,11028,11029,11030,11031,11032,11033,11034,11035,11036,11037,11038,11039,11040,11041,11042,11043,11044,11045,11046,11047,11048,11049,11050,11051,11052,11053,11054,11055,11056,11057,11058,11059,11060,11061,11062,11063,11064,11065,11066,11067,11068,11069,11070,11071,11072,11073,11074,11075,11076,11077,11078,11079,11080,11081,11082,11083,11084,11085,11086,11087,11088,11089,11090,11091,11092,11093,11094,11095,11096,11097,11098,11099,11100,11101,11102,11103,11104,11105,11106,11107,11108,11109,11110,11111,11112,11113,11114,11115,11116,11117,11118,11119,11120,11121,11122,11123,11124,11125,11126,11127,11128,11129,11130,11131,11132,11133,11134,11135,11136,11137,11138,11139,11140,11141,11142,11143,11144,11145,11146,11147,11148,11149,11150,11151,11152,11153,11154,11155,11156,11157,11158,11159,11160,11161,11162,11163,11164,11165,11166,11167,11168,11169,11170,11171,11172,11173,11174,11175,11176,11177,11178,11179,11180,11181,11182,11183,11184,11185,11186,11187,11188,11189,11190,11191,11192,11193,11194,11195,11196,11197,11198,11199,11200,11201,11202,11203,11204,11205,11206,11207,11208,11209,11210,11211,11212,11213,11214,11215,11216,11217,11218,11219,11220,11221,11222,11223,11224,11225,11226,11227,11228,11229,11230,11231,11232,11233,11234,11235,11236,11237,11238,11239,11240,11241,11242,11243,11244,11245,11246,11247,11248,11249,11250,11251,11252,11253,11254,11255,11256,11257,11258,11259,11260,11261,11262,11263,11264,11265,11266,11267,11268,11269,11270,11271,11272,11273,11274,11275,11276,11277,11278,11279,11280,11281,11282,11283,11284,11285,11286,11287,11288,11289,11290,11291,11292,11293,11294,11295,11296,11297,11298,11299,11300,11301,11302,11303,11304,11305,11306,11307,11308,11309,11310,11311,11312,11313,11314,11315,11316,11317,11318,11319,11320,11321,11322,11323,11324,11325,11326,11327,11328,11329,11330,11331,11332,11333,11334,11335,11336,11337,11338,11339,11340,11341,11342,11343,11344,11345,11346,11347,11348,11349,11350,11351,11352,11353,11354,11355,11356,11357,11358,11359,11360,11361,11362,11363,11364,11365,11366,11367,11368,11369,11370,11371,11372,11373,11374,11375,11376,11377,11378,11379,11380,11381,11382,11383,11384,11385,11386,11387,11388,11389,11390,11391,11392,11393,11394,11395,11396,11397,11398,11399,11400,11401,11402,11403,11404,11405,11406,11407,11408,11409,11410,11411,11412,11413,11414,11415,11416,11417,11418,11419,11421,11422,11423,11424,11425,11426,11427,11428,11429,11430,11431,11432,11433,11434,11435,11436,11437,11438,11439,11440,11441,11442,11443,11444,11445,11446,11447,11448,11449,11450,11451,11452,11453,11454,11455,11456,11457,11458,11459,11460,11461,11462,11463,11464,11465,11466,11467,11468,11469,11470,11471,11472,11473,11474,11475,11476,11477,11478,11479,11480,11481,11482,11483,11484,11485,11486,11487,11488,11489,11490,11491,11492,11493,11494,11495,11496,11497,11498,11499,11500,11501,11502,11503,11504,11505,11506,11507,11508,11509,11510,11511,11512,11513,11514,11515,11516,11517,11518,11519,11520,11521,11522,11523,11524,11525,11526,11527,11528,11529,11530,11531,11532,11533,11534,11535,11536,11537,11538,11539,11540,11541,11542,11543,11544,11545,11546,11547,11548,11549,11550,11551,11552,11553,11554,11555,11556,11557,11558,11559,11560,11561,11562,11563,11564,11565,11566,11567,11568,11569,11570,11571,11572,11573,11574,11575,11576,11577,11578,11579,11580,11581,11582,11583,11584,11585,11586,11587,11588,11589],{"category":10946},"Frontend",{"category":10948},"Heritage",{"category":10950},"AI",{"category":808},{"category":10953},"Business",{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10950},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":5607},{"category":808},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":4527},{"category":4527},{"category":10953},{"category":10953},{"category":10948},{"category":4527},{"category":10948},{"category":5607},{"category":4527},{"category":808},{"category":10953},{"category":7572},{"category":10950},{"category":10948},{"category":808},{"category":5607},{"category":808},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":7572},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":808},{"category":6183},{"category":10950},{"category":10950},{"category":10953},{"category":5607},{"category":10953},{"category":808},{"category":808},{"category":10953},{"category":808},{"category":5607},{"category":808},{"category":7572},{"category":7572},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10950},{"category":5607},{"category":10953},{"category":7572},{"category":7572},{"category":7572},{"category":10948},{"category":808},{"category":808},{"category":10948},{"category":10946},{"category":10950},{"category":7572},{"category":7572},{"category":4527},{"category":7572},{"category":10953},{"category":10950},{"category":10948},{"category":808},{"category":10948},{"category":5607},{"category":10948},{"category":5607},{"category":4527},{"category":10948},{"category":10948},{"category":808},{"category":10953},{"category":808},{"category":10946},{"category":808},{"category":808},{"category":808},{"category":808},{"category":10953},{"category":10953},{"category":10948},{"category":10946},{"category":4527},{"category":5607},{"category":4527},{"category":10946},{"category":808},{"category":808},{"category":7572},{"category":808},{"category":808},{"category":5607},{"category":808},{"category":7572},{"category":808},{"category":808},{"category":10948},{"category":10948},{"category":4527},{"category":5607},{"category":5607},{"category":6183},{"category":6183},{"category":6183},{"category":10953},{"category":808},{"category":7572},{"category":5607},{"category":10948},{"category":10948},{"category":7572},{"category":5607},{"category":5607},{"category":10946},{"category":808},{"category":10948},{"category":10948},{"category":808},{"category":10948},{"category":7572},{"category":7572},{"category":10948},{"category":4527},{"category":10948},{"category":5607},{"category":4527},{"category":5607},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":4527},{"category":808},{"category":7572},{"category":7572},{"category":10953},{"category":808},{"category":808},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":5607},{"category":5607},{"category":5607},{"category":808},{"category":10948},{"category":10948},{"category":10948},{"category":7572},{"category":10953},{"category":10948},{"category":10948},{"category":808},{"category":10948},{"category":808},{"category":10946},{"category":10948},{"category":10953},{"category":10953},{"category":808},{"category":808},{"category":10950},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":808},{"category":7572},{"category":7572},{"category":7572},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":10948},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10953},{"category":10953},{"category":10948},{"category":808},{"category":10946},{"category":5607},{"category":6183},{"category":10948},{"category":10948},{"category":4527},{"category":808},{"category":10948},{"category":10948},{"category":7572},{"category":10948},{"category":10946},{"category":7572},{"category":7572},{"category":4527},{"category":808},{"category":808},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":6183},{"category":10948},{"category":5607},{"category":808},{"category":808},{"category":10948},{"category":7572},{"category":10948},{"category":10948},{"category":10948},{"category":10946},{"category":10948},{"category":10948},{"category":808},{"category":10948},{"category":808},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":10950},{"category":10950},{"category":808},{"category":10948},{"category":7572},{"category":7572},{"category":10948},{"category":808},{"category":10948},{"category":10948},{"category":10950},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":808},{"category":808},{"category":808},{"category":4527},{"category":808},{"category":808},{"category":10946},{"category":808},{"category":10946},{"category":10946},{"category":4527},{"category":5607},{"category":808},{"category":5607},{"category":10948},{"category":10948},{"category":808},{"category":808},{"category":808},{"category":10953},{"category":808},{"category":808},{"category":10948},{"category":5607},{"category":10950},{"category":10950},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10953},{"category":808},{"category":10948},{"category":10948},{"category":808},{"category":808},{"category":10946},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":808},{"category":5607},{"category":10948},{"category":10953},{"category":10950},{"category":10948},{"category":10953},{"category":4527},{"category":10948},{"category":4527},{"category":808},{"category":7572},{"category":10948},{"category":10948},{"category":808},{"category":10948},{"category":5607},{"category":10948},{"category":10948},{"category":808},{"category":10953},{"category":808},{"category":808},{"category":808},{"category":808},{"category":10953},{"category":808},{"category":808},{"category":10953},{"category":7572},{"category":808},{"category":10950},{"category":10948},{"category":10948},{"category":808},{"category":808},{"category":10948},{"category":10948},{"category":10948},{"category":10950},{"category":808},{"category":808},{"category":5607},{"category":10946},{"category":808},{"category":10948},{"category":808},{"category":5607},{"category":10953},{"category":10953},{"category":10946},{"category":10946},{"category":10948},{"category":10953},{"category":4527},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":808},{"category":808},{"category":5607},{"category":808},{"category":808},{"category":808},{"category":11420},"Programming",{"category":808},{"category":808},{"category":5607},{"category":5607},{"category":808},{"category":808},{"category":10953},{"category":4527},{"category":808},{"category":10953},{"category":808},{"category":808},{"category":808},{"category":808},{"category":7572},{"category":5607},{"category":10953},{"category":10953},{"category":808},{"category":808},{"category":10953},{"category":808},{"category":4527},{"category":10953},{"category":808},{"category":808},{"category":5607},{"category":5607},{"category":10948},{"category":10953},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":10946},{"category":10948},{"category":7572},{"category":4527},{"category":4527},{"category":4527},{"category":4527},{"category":4527},{"category":4527},{"category":10948},{"category":808},{"category":7572},{"category":5607},{"category":7572},{"category":5607},{"category":808},{"category":10946},{"category":10948},{"category":5607},{"category":10946},{"category":10948},{"category":10948},{"category":10948},{"category":5607},{"category":5607},{"category":5607},{"category":10953},{"category":10953},{"category":10953},{"category":5607},{"category":5607},{"category":10953},{"category":10953},{"category":10953},{"category":10948},{"category":4527},{"category":808},{"category":7572},{"category":808},{"category":10948},{"category":10953},{"category":10953},{"category":10948},{"category":10948},{"category":5607},{"category":808},{"category":5607},{"category":5607},{"category":5607},{"category":10946},{"category":808},{"category":10948},{"category":10948},{"category":10953},{"category":10953},{"category":5607},{"category":808},{"category":6183},{"category":5607},{"category":6183},{"category":10953},{"category":10948},{"category":5607},{"category":10948},{"category":10948},{"category":10948},{"category":808},{"category":808},{"category":10948},{"category":10950},{"category":10950},{"category":7572},{"category":10948},{"category":10948},{"category":10948},{"category":10948},{"category":808},{"category":808},{"category":10946},{"category":808},{"category":4527},{"category":5607},{"category":10946},{"category":10946},{"category":808},{"category":808},{"category":10946},{"category":10946},{"category":10946},{"category":4527},{"category":808},{"category":808},{"category":10953},{"category":808},{"category":5607},{"category":10948},{"category":10948},{"category":5607},{"category":10948},{"category":10948},{"category":5607},{"category":10948},{"category":808},{"category":10948},{"category":4527},{"category":10948},{"category":10948},{"category":10948},{"category":7572},{"category":7572},{"category":4527},1772951194506]