Field Notes จากงานระบบจริง: สิ่งที่ทีมมักเจอช้าเกินไป และควรเห็นให้เร็วขึ้น
หลายปัญหาในงานพัฒนาระบบไม่ได้เริ่มจาก bug ใหญ่หรือ incident ใหญ่ แต่เริ่มจากสัญญาณเล็ก ๆ ที่ถูกมองข้าม เช่น requirement ที่ยังพูดกันไม่ตรง, endpoint ที่ดูเหมือนใช้ได้แต่ contract เริ่มไหล, deployment ที่ผ่าน CI แต่ไม่มีใครมั่นใจจริง, หรือ feature ที่ปล่อยไปแล้วมีคนใช้น้อยกว่าที่คิดมาก
สิ่งที่ทีมมักเรียนรู้จากงานจริงไม่ใช่แค่ “ต้องเขียนโค้ดให้ดี” แต่คือ ต้องมองระบบทั้งเส้นทาง ตั้งแต่ input, decision, integration, runtime behavior, operation ไปจนถึง feedback หลังปล่อยของจริง
บทความนี้รวบรวม field notes จาก pattern ที่เจอบ่อยในงานพัฒนาระบบจริง โดยไม่ได้เน้นทฤษฎีอย่างเดียว แต่เน้นมุมที่เอาไปเช็คทีม เช็คระบบ และใช้ตั้งคำถามก่อนปัญหาจะลุกลาม
1) Requirement ที่ดูเหมือนชัด มักยังไม่ชัดในจุดที่สำคัญที่สุด
เวลาทีมคุย requirement กัน มักตกลงกันได้เร็วในระดับหน้าจอหรือ happy path แต่ปัญหาจริงจะไปโผล่ตรงนี้
- ถ้าข้อมูลไม่ครบ ระบบควรทำอย่างไร
- ถ้าบริการภายนอกช้า ระบบจะ timeout ที่จุดไหน
- ถ้าผู้ใช้ย้อนกลับมาแก้ข้อมูลเดิม ต้อง replace, append หรือ version
- ถ้าสถานะเปลี่ยนพร้อมกันจากหลายฝั่ง อะไรคือ source of truth
- ถ้าธุรกรรมล้มกลางทาง ระบบต้อง rollback หรือ mark pending
คำว่า “ระบบต้องรองรับ” ถ้าไม่แปลลงเป็น behavior ที่วัดได้ มันยังไม่ใช่ requirement ที่พร้อมพัฒนา
ตัวอย่างคำถามที่ช่วยทำให้ requirement ชัดขึ้น:
- Input ที่เลวร้ายที่สุดคืออะไร
- มี external dependency กี่ตัว และตัวไหนสำคัญสุด
- อะไรคือสถานะที่ห้ามเกิดขึ้น
- ถ้า retry ซ้ำ ระบบยังถูกต้องหรือไม่
- มีข้อมูลอะไรที่ต้อง audit ย้อนหลังได้
หลายครั้งปัญหาไม่ได้อยู่ที่ทีมไม่เก่ง แต่ requirement ยังไม่ถูกบีบให้ชัดในระดับ behavior
2) Integration ที่พังบ่อย ไม่ได้พังเพราะ API ล่มอย่างเดียว
ระบบจริงมักพึ่ง service อื่นเสมอ ไม่ว่าจะเป็น payment, auth, email, analytics, CRM, storage หรือ data warehouse และ integration ที่ล้มบ่อยที่สุดมักไม่ใช่ “service ปลายทางล่ม” แต่เป็นเรื่องต่อไปนี้
- request shape เปลี่ยนเล็กน้อย
- field ใหม่มาแต่ไม่ได้ handle
- field เดิมกลายเป็น nullable
- timeout ไม่พอเมื่อโหลดสูง
- retry แล้วสร้างข้อมูลซ้ำ
- signature ผ่านใน sandbox แต่ fail ใน production
- event มาถึงไม่เรียงลำดับ
ตัวอย่าง wrapper สำหรับเรียก external API แบบมี timeout, retry และ logging ให้พอ trace ได้:
import axios from "axios";
const client = axios.create({
baseURL: process.env.CRM_BASE_URL,
timeout: 5000,
headers: {
Authorization: `Bearer ${process.env.CRM_API_KEY}`,
"Content-Type": "application/json"
}
});
export async function createLead(payload: Record<string, unknown>) {
const startedAt = Date.now();
try {
const response = await client.post("/leads", payload, {
headers: {
"X-Request-Id": crypto.randomUUID()
}
});
console.info("crm.createLead.success", {
status: response.status,
durationMs: Date.now() - startedAt
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("crm.createLead.failed", {
status: error.response?.status,
body: error.response?.data,
durationMs: Date.now() - startedAt
});
} else {
console.error("crm.createLead.failed", {
error,
durationMs: Date.now() - startedAt
});
}
throw error;
}
}
โค้ดแบบนี้ไม่ได้ทำให้ integration สมบูรณ์ทันที แต่ทำให้เวลาเกิดปัญหา เรารู้เร็วขึ้นว่าพังตรงไหน พังอย่างไร และน่าจะเกี่ยวกับ request, dependency หรือ latency
3) สิ่งที่ deploy ได้ ไม่ได้แปลว่าพร้อมปล่อยจริง
ทีมจำนวนมากมี CI/CD แล้ว แต่ยังไม่มี release discipline ที่ดีพอ ความต่างสำคัญคือ
- deployable = merge แล้ว build ผ่าน
- releasable = merge แล้วพร้อมปล่อยโดยมี confidence ที่พอ
สิ่งที่ควรมีอย่างน้อยก่อนปล่อยงานจริง:
- migration ปลอดภัยหรือไม่
- feature flag ครอบหรือยัง
- monitoring ของ flow สำคัญพร้อมหรือยัง
- rollback หรือ kill switch ทำได้หรือไม่
- data backfill หรือ cleanup ต้องทำก่อนหรือหลัง deploy
- ทีม support หรือ ops รู้หรือยังว่ามีพฤติกรรมใหม่เข้ามา
ตัวอย่าง feature flag แบบง่ายใน Node.js:
const featureFlags = {
newCheckoutFlow: process.env.FEATURE_NEW_CHECKOUT_FLOW === "true"
};
export function isFeatureEnabled(name: keyof typeof featureFlags) {
return featureFlags[name] === true;
}
ใช้ใน route:
app.post("/checkout", async (req, res) => {
if (isFeatureEnabled("newCheckoutFlow")) {
return handleNewCheckout(req, res);
}
return handleLegacyCheckout(req, res);
});
feature flag ไม่ได้ช่วยแทนคุณภาพโค้ด แต่ช่วยลด blast radius ตอน roll out และช่วยให้ revert ทาง behavior ได้เร็วกว่า revert code อย่างเดียว
4) ปัญหา performance มักเริ่มจาก query และ data shape ก่อนจะเริ่มจาก infra
เวลาระบบช้า ทีมมักคิดถึง CPU, RAM, autoscaling ก่อน แต่หลายปัญหาเริ่มจาก
- query ดึงข้อมูลมากเกินไป
- pagination ไม่มีจริง
- หน้าเดียวเรียก API มากเกินจำเป็น
- serializer หนักเกินไป
- nested loop กับ data set ใหญ่
- cache key ออกแบบไม่ดี
ตัวอย่าง anti-pattern:
const users = await db.user.findMany({
include: {
orders: true,
payments: true,
activities: true,
documents: true
}
});
โค้ดนี้อาจ “ทำงานได้” ใน dev แต่พอข้อมูลจริงโตขึ้น มันจะเริ่มเป็นภาระทั้ง query time, memory และ payload size
แนวทางที่ปลอดภัยกว่า:
const users = await db.user.findMany({
select: {
id: true,
name: true,
email: true,
createdAt: true
},
take: 50,
orderBy: {
createdAt: "desc"
}
});
แล้วค่อย fetch รายละเอียดเฉพาะจุดเมื่อผู้ใช้เปิดดูต่อ การออกแบบให้หน้าจอขอเท่าที่ต้องใช้จริง มักแก้ performance ได้มากกว่าการอัดเครื่องอย่างเดียว
5) Logging ที่ดีไม่ใช่ log ทุกอย่าง แต่ log สิ่งที่ช่วยตอบคำถาม
เวลาระบบมีปัญหา คำถามที่ทีมต้องตอบมักเป็นแบบนี้
- request นี้เข้ามาเมื่อไร
- ใครเป็นคนเรียก
- ผ่านขั้นตอนไหนแล้วบ้าง
- พังตรง service ไหน
- เป็นปัญหากับผู้ใช้บางกลุ่มหรือทุกคน
- เกิดหลัง deploy ไหน
ดังนั้น log ที่ดีควรมี context ที่พอ trace ได้ เช่น
- requestId
- userId หรือ actorId
- route หรือ operation name
- dependency target
- durationMs
- result / status
- error code
ตัวอย่าง middleware สำหรับ request correlation:
import { randomUUID } from "node:crypto";
import type { NextFunction, Request, Response } from "express";
export function requestContext(req: Request, res: Response, next: NextFunction) {
const requestId = req.header("x-request-id") || randomUUID();
res.locals.requestId = requestId;
res.setHeader("x-request-id", requestId);
next();
}
และตอน log:
console.info("order.create.started", {
requestId: res.locals.requestId,
userId: req.user?.id,
route: req.originalUrl
});
ถ้า log ไม่มี context พอ เวลาพังจริงมันจะกลายเป็น log ที่เปิดดูแล้วไม่ช่วยอะไร
6) Error handling ที่ดีต้องแยกระหว่าง user error, system error และ domain error
หลายระบบตอบทุกอย่างเป็น 500 หรือส่งข้อความ vague เช่น “something went wrong” จนทั้งผู้ใช้และทีม debug ลำบาก
แนวคิดที่ช่วยมากคือแยก error เป็นอย่างน้อย 3 ประเภท
User error
ข้อมูลไม่ถูกต้อง, กรอกไม่ครบ, รูปแบบผิด
Domain error
ผิดกฎธุรกิจ เช่น ยอดคืนเงินเกินยอดจ่าย, สถานะไม่อนุญาตให้ยกเลิกแล้ว
System error
ฐานข้อมูลล่ม, service ภายนอก timeout, queue ไม่ตอบ
ตัวอย่างโครงสร้าง error แบบง่าย:
export class AppError extends Error {
constructor(
public code: string,
public status: number,
message: string,
public details?: Record<string, unknown>
) {
super(message);
}
}
export class ValidationError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super("VALIDATION_ERROR", 400, message, details);
}
}
export class DomainError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super("DOMAIN_ERROR", 409, message, details);
}
}
middleware จัดการ error:
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
if (error instanceof AppError) {
return res.status(error.status).json({
code: error.code,
message: error.message,
details: error.details ?? null
});
}
console.error("unhandled_error", { error });
return res.status(500).json({
code: "INTERNAL_SERVER_ERROR",
message: "Unexpected server error"
});
});
โครงสร้างแบบนี้ช่วยให้ทั้ง frontend, backend, support และ monitoring คุยกันด้วยภาษาที่ชัดขึ้น
7) ระบบที่ดูนิ่ง อาจแค่ยังไม่มี feedback loop ที่เร็วพอ
บาง feature ไม่ได้พังแบบ error แต่พังแบบ “ไม่มีผลลัพธ์” เช่น
- ผู้ใช้ไม่กด CTA
- ฟอร์มยาวเกินไปจน drop
- onboarding จบไม่ถึงขั้นสำคัญ
- alert ขึ้นเยอะจนไม่มีใครสนใจ
- report ถูก generate แต่ไม่มีใครอ่าน
นี่คือปัญหาที่ไม่เห็นใน server log อย่างเดียว ต้องมี product feedback loop ด้วย เช่น
- conversion ต่อ step
- success rate ของงานหลัก
- time to complete
- abandonment rate
- top validation failures
ตัวอย่าง event tracking แบบบางและพอใช้งานจริง:
export function trackEvent(name: string, payload: Record<string, unknown>) {
console.info("analytics.event", {
name,
...payload,
createdAt: new Date().toISOString()
});
}
ใช้ใน flow:
trackEvent("checkout_started", {
userId,
quoteId,
packageType
});
trackEvent("checkout_completed", {
userId,
orderId,
amount
});
ต่อให้ยังไม่มี analytics stack ใหญ่ การเริ่มจาก event สำคัญไม่กี่ตัวก็ช่วยให้เห็น health ของ flow ดีขึ้นมาก
8) Data ที่เก็บไว้เพื่อใช้ทีหลัง ต้องออกแบบตั้งแต่วันนี้
ปัญหาคลาสสิกคือระบบทำงานได้ แต่พอ 3 เดือนผ่านไปแล้วมีคำถามใหม่ เช่น
- ลูกค้ากลุ่มไหนยกเลิกบ่อย
- deployment ไหนทำ conversion ตก
- ทีม support ใช้เวลาปิดเคสกี่ชั่วโมง
- order ไหนถูก retry ซ้ำเกินปกติ
ถ้าตอนแรกไม่ได้เก็บ field สำคัญไว้เลย คำถามพวกนี้จะตอบยากมาก
ข้อมูลที่ควรเก็บไว้ตั้งแต่ต้นใน flow สำคัญ:
- createdAt / updatedAt
- actor หรือ source
- status transitions
- external reference IDs
- idempotency key
- version ของ business rule หรือ pricing rule
- request/response snapshot บางส่วนเท่าที่เหมาะสม
ตัวอย่าง schema ฝั่งธุรกรรม:
create table payment_attempts (
id uuid primary key,
order_id uuid not null,
provider text not null,
provider_reference text,
idempotency_key text,
status text not null,
amount numeric(12,2) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
ไม่ใช่ทุกระบบต้องเก็บทุกอย่าง แต่ flow ที่มีผลต่อเงิน, สถานะ, compliance หรือ dispute ควรถูกออกแบบให้ audit ย้อนหลังได้
9) ทีมที่โตได้ ไม่ได้โตจากการเขียนเร็วอย่างเดียว แต่โตจากการลด ambiguity
งานจริงไม่ค่อยพังเพราะ syntax error อย่างเดียว แต่มักพังเพราะความกำกวมสะสม เช่น
- owner ของ flow ไม่ชัด
- field นี้จริง ๆ ใครดูแล
- source of truth อยู่ตรงไหน
- API เปลี่ยนได้แค่ไหนโดยไม่ประกาศ
- migration ต้องประสานกับใครบ้าง
- deploy ลำดับไหนก่อนหลัง
ทีมที่ส่งงานได้ต่อเนื่องมักมีลักษณะร่วมกันคือ
- ตั้งชื่อชัด
- แยก boundary ชัด
- เขียน assumption ไว้
- มี runbook หรือ checklist ให้เรื่องที่พลาดบ่อย
- ตกลง policy เรื่อง merge, release, rollback และ incident ให้ตรงกัน
ตัวอย่าง release checklist แบบสั้น:
- migration ผ่านใน staging แล้ว
- feature flag พร้อม
- dashboard และ alert พร้อม
- rollback plan มี
- support note อัปเดตแล้ว
- owner หลัง deploy ชัดเจน
checklist ไม่ได้ทำให้ทีมช้า แต่ช่วยลดการลืมเรื่องสำคัญที่มีต้นทุนสูงเวลาเกิดปัญหา
10) สิ่งที่ควรดูหลัง deploy ไม่ใช่แค่ “server ยังไม่ล้ม”
หลัง deploy ทีมจำนวนมากดูแค่ว่า health check ผ่านและ error 500 ไม่ขึ้น แต่จริง ๆ ควรดูเพิ่มอย่างน้อย
- success rate ของ flow หลัก
- p95 latency ของ endpoint สำคัญ
- queue backlog
- job failure rate
- payment / webhook / sync error
- conversion ของ feature ใหม่
- สัดส่วน validation error ที่พุ่งขึ้นผิดปกติ
ตัวอย่าง metric logging แบบง่าย:
function logMetric(name: string, value: number, tags: Record<string, string> = {}) {
console.info("metric", {
name,
value,
tags,
createdAt: new Date().toISOString()
});
}
logMetric("checkout.duration_ms", 842, { env: "production" });
logMetric("webhook.failed", 1, { provider: "stripe" });
ถ้ายังไม่มี metrics platform เต็มรูปแบบ อย่างน้อยการเก็บ structured logs ที่ parse ต่อได้ก็ยังดีกว่าไม่มีอะไรเลย
ตัวอย่าง architecture mindset ที่ใช้ได้จริง
เวลาจะประเมิน feature ใหม่ ลองมองผ่านคำถามนี้ก่อนเริ่มลงมือ:
- input ของระบบมาจากไหน
- validation เกิดที่ชั้นไหน
- มี dependency อะไรบ้าง
- state change สำคัญคืออะไร
- จุดไหนต้อง idempotent
- ถ้าพังกลางทางจะ recover อย่างไร
- อะไรที่ต้องเห็นใน log และ metric
- ต้อง audit ย้อนหลังได้หรือไม่
- rollout แบบค่อย ๆ เปิดได้ไหม
- ถ้าต้อง rollback จะ rollback behavior, code หรือ data
การคิดแบบนี้ช่วยให้ทีมออกแบบระบบจาก “ของที่ทำงานวันนี้” ไปสู่ “ของที่ยังพอควบคุมได้เมื่อระบบโตขึ้น”
สรุป
Field notes ที่มีค่าที่สุดไม่ใช่คำแนะนำแบบสวยงาม แต่เป็นบทเรียนจากสิ่งที่ทีมมักพลาดซ้ำโดยไม่รู้ตัว
สิ่งที่ควรเห็นให้เร็วขึ้นเสมอคือ:
- requirement ยังคลุมเครือตรงไหน
- integration เปราะบางตรงไหน
- flow ไหนยังไม่มี observability พอ
- data ไหนควรเก็บตั้งแต่ตอนนี้
- release ไหนเสี่ยงเกินกว่าจะปล่อยแบบไม่ guard
- ปัญหาไหนเป็นเรื่อง code และปัญหาไหนเป็นเรื่องระบบการทำงาน
สุดท้าย งานระบบจริงไม่ใช่แค่การเขียน feature ให้เสร็จ แต่คือการทำให้ระบบ พอเชื่อถือได้, พอ debug ได้, พอปล่อยซ้ำได้ และพอส่งต่อให้ทีมทำงานร่วมกันได้โดยไม่พังง่าย
ถ้าทีมเริ่มมองเห็นสัญญาณเล็ก ๆ ได้เร็วขึ้น ปัญหาใหญ่จำนวนมากจะไม่ต้องรอให้เกิดก่อนแล้วค่อยเรียนรู้