

บทนำ: ทำไมต้อง GraphQL Subscriptions บน Hexagonal Architecture?
ในโลกของการพัฒนาเว็บแอปพลิเคชันยุค 2026 ความต้องการระบบแบบ Real-time กลายเป็นสิ่งจำเป็นอย่างหลีกเลี่ยงไม่ได้ ไม่ว่าจะเป็นแพลตฟอร์มแชท การแจ้งเตือนสด ระบบติดตามสถานะ หรือแดชบอร์ดทางการเงิน การส่งข้อมูลแบบดั้งเดิมด้วย HTTP Polling หรือ WebSocket แบบธรรมดาเริ่มไม่ตอบโจทย์ในแง่ของประสิทธิภาพและความยืดหยุ่น
GraphQL Subscriptions ได้เข้ามาเปลี่ยนโฉมการสื่อสารแบบ Real-time โดยอนุญาตให้ Client สมัครรับข้อมูลเฉพาะที่ต้องการ และเซิร์ฟเวอร์จะ Push ข้อมูลเมื่อเกิดเหตุการณ์ที่เกี่ยวข้อง โดยไม่ต้องร้องขอซ้ำแล้วซ้ำเล่า
แต่ความท้าทายที่แท้จริงคือการออกแบบระบบที่มีความยืดหยุ่น ทดสอบง่าย และสามารถปรับเปลี่ยนเทคโนโลยีได้โดยไม่กระทบ Business Logic นั่นคือที่มาของ Hexagonal Architecture (หรือที่รู้จักในชื่อ Ports and Adapters) ซึ่งช่วยแยก Core Business Logic ออกจาก Infrastructure Layer อย่างชัดเจน
บทความนี้จะพาคุณไปรู้จักการรวมพลังของ GraphQL Subscriptions และ Hexagonal Architecture อย่างลึกซึ้ง พร้อมตัวอย่างโค้ดจริง แนวทางปฏิบัติที่ดีที่สุด และกรณีการใช้งานในโลกความจริง
1. ทำความเข้าใจ Hexagonal Architecture ในบริบทของ GraphQL
1.1 หลักการพื้นฐานของ Hexagonal Architecture
Hexagonal Architecture ถูกคิดค้นโดย Alistair Cockburn โดยมีแนวคิดหลักคือการแยก “Core Domain” (Business Logic) ออกจาก “External Concerns” (ฐานข้อมูล, API ภายนอก, UI) โดยใช้แนวคิดของ Ports (อินเทอร์เฟซที่กำหนดพฤติกรรมที่ต้องการ) และ Adapters (การนำอินเทอร์เฟซนั้นไปใช้งานจริง)
- Inside (Core Domain): ประกอบด้วย Entities, Use Cases, Domain Services — ไม่มีการพึ่งพา Framework หรือ Library ภายนอก
- Outside (Infrastructure): ฐานข้อมูล, GraphQL Schema, Message Queue, HTTP Clients
- Ports: อินเทอร์เฟซที่ Core กำหนด เช่น IOrderRepository, INotificationService
- Adapters: การ implement Ports ด้วยเทคโนโลยีจริง เช่น PostgreSQLAdapter, RedisAdapter, GraphQLAdapter
1.2 GraphQL Subscriptions ทำงานอย่างไร
GraphQL Subscriptions ใช้ WebSocket เป็นช่องทางหลักในการสื่อสาร โดย Client จะส่ง subscription query เพื่อบอกเซิร์ฟเวอร์ว่าสนใจเหตุการณ์ใด เมื่อเหตุการณ์นั้นเกิดขึ้น เซิร์ฟเวอร์จะ Push ข้อมูลกลับไปยัง Client ที่สมัครรับข้อมูลไว้
ในระดับสถาปัตยกรรม Subscription ประกอบด้วย:
- Subscription Resolver: ฟังก์ชันที่กำหนด Event ที่ Client สนใจ
- Pub/Sub Engine: ระบบจัดการการส่งต่อข้อความระหว่าง Publisher และ Subscriber (เช่น Redis Pub/Sub, In-memory EventEmitter)
- Trigger Mechanism: จุดที่ Business Logic ส่งสัญญาณไปยัง Pub/Sub Engine เมื่อเกิดเหตุการณ์
1.3 ปัญหาเมื่อรวม GraphQL Subscriptions โดยตรงกับ Business Logic
การเขียน Subscription Resolver ที่เรียกใช้ Database โดยตรง หรือการผูก Business Logic กับ GraphQL Schema ทำให้เกิดปัญหา:
- ไม่สามารถทดสอบ Business Logic โดยไม่ต้องมี GraphQL Server
- เปลี่ยนจาก GraphQL เป็น REST หรือ gRPC ได้ยาก
- เกิด Coupling ระหว่าง Layer สูง
Hexagonal Architecture แก้ปัญหาเหล่านี้โดยการสร้าง Abstraction Layer ระหว่าง Business Logic และ GraphQL Adapter
2. การออกแบบ Core Domain สำหรับ Subscription Events
2.1 กำหนด Domain Events
หัวใจสำคัญของ Subscription ใน Hexagonal Architecture คือ Domain Events — อีเวนต์ที่เกิดขึ้นใน Core Domain และมีความหมายทางธุรกิจ
// typescript
// src/domain/events/order-events.ts
export interface OrderPlacedEvent {
eventType: 'ORDER_PLACED';
orderId: string;
customerId: string;
items: OrderItem[];
totalAmount: number;
timestamp: Date;
}
export interface OrderStatusChangedEvent {
eventType: 'ORDER_STATUS_CHANGED';
orderId: string;
previousStatus: OrderStatus;
newStatus: OrderStatus;
updatedBy: string;
timestamp: Date;
}
// Domain Event Bus (Port)
export interface IDomainEventBus {
publish<T extends BaseEvent>(event: T): Promise<void>;
subscribe<T extends BaseEvent>(
eventType: string,
handler: (event: T) => Promise<void>
): void;
unsubscribe(eventType: string, handler: Function): void;
}
2.2 Use Case ที่สร้าง Event
Use Case จะเป็นตัวจัดการ Business Logic และเรียกใช้ Domain Event Bus เมื่อทำงานสำเร็จ
// typescript
// src/application/use-cases/place-order.use-case.ts
import { IOrderRepository } from '../ports/order-repository.port';
import { IDomainEventBus } from '../ports/domain-event-bus.port';
export class PlaceOrderUseCase {
constructor(
private readonly orderRepo: IOrderRepository,
private readonly eventBus: IDomainEventBus
) {}
async execute(input: PlaceOrderInput): Promise<Order> {
// Business logic validation
const order = Order.create(input);
// Persist
await this.orderRepo.save(order);
// Publish domain event
await this.eventBus.publish({
eventType: 'ORDER_PLACED',
orderId: order.id,
customerId: order.customerId,
items: order.items,
totalAmount: order.totalAmount,
timestamp: new Date()
});
return order;
}
}
2.3 Port สำหรับ Subscription Service
เรากำหนด Port ที่ Core ต้องการสำหรับการ Push ข้อมูลไปยัง Client โดยไม่ต้องรู้ว่าเป็น GraphQL หรือเทคโนโลยีอื่น
// typescript
// src/application/ports/subscription-service.port.ts
export interface ISubscriptionService {
// Register a client to receive events
registerClient(clientId: string, topics: string[]): void;
// Remove client
unregisterClient(clientId: string): void;
// Push event to all subscribed clients
pushToTopic<T>(topic: string, payload: T): Promise<void>;
// Get active subscriptions for monitoring
getActiveSubscriptions(): Promise<SubscriptionInfo[]>;
}
export interface SubscriptionInfo {
clientId: string;
topics: string[];
connectedAt: Date;
}
3. การสร้าง Adapter สำหรับ GraphQL Subscriptions
3.1 Pub/Sub Engine Adapter
เราเลือกใช้ Redis Pub/Sub เป็นกลไกกระจายข้อความระหว่าง Instance ต่างๆ (ในกรณี Production ที่มีหลายเซิร์ฟเวอร์)
// typescript
// src/infrastructure/pubsub/redis-pubsub.adapter.ts
import { Redis } from 'ioredis';
import { IDomainEventBus } from '../../domain/events/domain-event-bus.port';
export class RedisPubSubAdapter implements IDomainEventBus {
private publisher: Redis;
private subscriber: Redis;
private handlers: Map<string, Function[]> = new Map();
constructor(redisUrl: string) {
this.publisher = new Redis(redisUrl);
this.subscriber = new Redis(redisUrl);
// Listen for all events
this.subscriber.on('message', (channel, message) => {
const event = JSON.parse(message);
const handlers = this.handlers.get(channel) || [];
handlers.forEach(handler => handler(event));
});
}
async publish<T>(event: T): Promise<void> {
const channel = (event as any).eventType;
await this.publisher.publish(channel, JSON.stringify(event));
}
subscribe(eventType: string, handler: Function): void {
const existing = this.handlers.get(eventType) || [];
existing.push(handler);
this.handlers.set(eventType, existing);
// Subscribe to Redis channel if first handler
if (existing.length === 1) {
this.subscriber.subscribe(eventType);
}
}
unsubscribe(eventType: string, handler: Function): void {
const existing = this.handlers.get(eventType) || [];
const filtered = existing.filter(h => h !== handler);
if (filtered.length === 0) {
this.subscriber.unsubscribe(eventType);
this.handlers.delete(eventType);
} else {
this.handlers.set(eventType, filtered);
}
}
}
3.2 GraphQL Subscription Resolver (Adapter)
ส่วนนี้เป็น Adapter ที่เชื่อมต่อระหว่าง GraphQL Schema กับ Domain Event Bus และ Subscription Service
// typescript
// src/infrastructure/graphql/subscriptions/order.subscription.ts
import { withFilter } from 'graphql-subscriptions';
import { ISubscriptionService } from '../../../application/ports/subscription-service.port';
import { IDomainEventBus } from '../../../domain/events/domain-event-bus.port';
export class OrderSubscriptionResolver {
constructor(
private readonly eventBus: IDomainEventBus,
private readonly subscriptionService: ISubscriptionService
) {
this.setupEventForwarding();
}
private setupEventForwarding(): void {
// Forward domain events to subscription service
this.eventBus.subscribe('ORDER_PLACED', async (event) => {
await this.subscriptionService.pushToTopic(
`order.${event.orderId}`,
{
type: 'ORDER_PLACED',
order: {
id: event.orderId,
customerId: event.customerId,
totalAmount: event.totalAmount
}
}
);
});
this.eventBus.subscribe('ORDER_STATUS_CHANGED', async (event) => {
await this.subscriptionService.pushToTopic(
`order.${event.orderId}`,
{
type: 'ORDER_STATUS_CHANGED',
orderId: event.orderId,
previousStatus: event.previousStatus,
newStatus: event.newStatus
}
);
});
}
// GraphQL Resolver methods
resolvers = {
Subscription: {
orderUpdated: {
subscribe: withFilter(
(parent, args, context) => {
const { pubsub } = context;
const orderId = args.orderId;
// Register client for this order
this.subscriptionService.registerClient(
context.clientId,
[`order.${orderId}`]
);
// Return async iterator for GraphQL
return pubsub.asyncIterator(`order.${orderId}`);
},
(payload, variables) => {
// Filter: only send if matches client's requested order
return payload.orderId === variables.orderId;
}
)
},
orderStatusChanged: {
subscribe: (parent, args, context) => {
const { pubsub } = context;
const orderId = args.orderId;
this.subscriptionService.registerClient(
context.clientId,
[`order.${orderId}`]
);
return pubsub.asyncIterator(`order.${orderId}`);
}
}
}
};
}
3.3 การเชื่อมต่อทั้งหมดผ่าน Dependency Injection
ในไฟล์หลักของแอปพลิเคชัน เราจะประกอบ Adapter ต่างๆ เข้าด้วยกัน
// typescript
// src/infrastructure/di/container.ts
import { RedisPubSubAdapter } from '../pubsub/redis-pubsub.adapter';
import { GraphQLSubscriptionService } from '../graphql/graphql-subscription.service';
import { OrderSubscriptionResolver } from '../graphql/subscriptions/order.subscription';
import { PlaceOrderUseCase } from '../../application/use-cases/place-order.use-case';
import { PostgresOrderRepository } from '../database/postgres-order.repository';
// 1. Create infrastructure adapters
const eventBus = new RedisPubSubAdapter(process.env.REDIS_URL!);
const orderRepo = new PostgresOrderRepository(process.env.DATABASE_URL!);
const subscriptionService = new GraphQLSubscriptionService(eventBus);
// 2. Create business logic (use cases)
const placeOrderUseCase = new PlaceOrderUseCase(orderRepo, eventBus);
// 3. Create GraphQL resolvers
const orderSubscription = new OrderSubscriptionResolver(
eventBus,
subscriptionService
);
// 4. Export for Apollo Server setup
export {
placeOrderUseCase,
orderSubscription,
eventBus
};
4. การจัดการ Authentication และ Authorization ใน Subscription
4.1 ปัญหาด้านความปลอดภัยของ WebSocket
GraphQL Subscriptions ที่ใช้ WebSocket มีความท้าทายด้านความปลอดภัยเฉพาะตัว:
- Token ต้องถูกส่งใน Connection Params (ไม่สามารถส่ง Header ได้)
- ต้อง validate token ตอนเชื่อมต่อ และตอน subscribe แต่ละครั้ง
- สิทธิ์การเข้าถึงข้อมูลอาจเปลี่ยนระหว่างการเชื่อมต่อ
4.2 Hexagonal Approach สำหรับ Auth
เราแยก Authentication Logic ไว้ใน Port และ Adapter เช่นเดียวกับส่วนอื่นๆ
// typescript
// src/application/ports/auth-service.port.ts
export interface IAuthService {
validateToken(token: string): Promise<TokenPayload | null>;
hasPermission(userId: string, resource: string, action: string): Promise<boolean>;
}
// src/infrastructure/auth/jwt-auth.adapter.ts
import { IAuthService } from '../../application/ports/auth-service.port';
import jwt from 'jsonwebtoken';
export class JwtAuthAdapter implements IAuthService {
constructor(private readonly secretKey: string) {}
async validateToken(token: string): Promise<TokenPayload | null> {
try {
const decoded = jwt.verify(token, this.secretKey) as TokenPayload;
return decoded;
} catch {
return null;
}
}
async hasPermission(userId: string, resource: string, action: string): Promise<boolean> {
// In real app, check against permission matrix in DB
// For demo, return true if user is admin
const user = await this.getUser(userId);
return user.role === 'admin' || user.permissions.includes(`${resource}:${action}`);
}
}
// ใน GraphQL Context Factory
// src/infrastructure/graphql/context.ts
export const createSubscriptionContext = async (
connectionParams: Record<string, any>,
webSocket: WebSocket,
authService: IAuthService
): Promise<GraphQLContext> => {
const token = connectionParams?.authToken;
if (!token) {
throw new Error('Authentication required');
}
const payload = await authService.validateToken(token);
if (!payload) {
throw new Error('Invalid or expired token');
}
return {
userId: payload.userId,
roles: payload.roles,
clientId: `${payload.userId}-${Date.now()}`,
isAuthenticated: true
};
};
4.3 การกรองข้อมูลตามสิทธิ์ (Authorization Filter)
ใน Subscription Resolver เราสามารถเพิ่ม Filter เพื่อให้แน่ใจว่า Client ได้รับเฉพาะข้อมูลที่ตนมีสิทธิ์
// typescript
// ใน OrderSubscriptionResolver
const orderUpdatedWithAuth = {
subscribe: withFilter(
(parent, args, context) => {
// Validate client can access this order
if (!context.isAuthenticated) {
throw new Error('Not authenticated');
}
// Register for topic
return pubsub.asyncIterator(`order.${args.orderId}`);
},
async (payload, variables, context) => {
// Double-check permission on each event
const hasAccess = await authService.hasPermission(
context.userId,
`order:${payload.orderId}`,
'read'
);
return hasAccess && payload.orderId === variables.orderId;
}
)
};
5. การทดสอบและ Monitoring
5.1 การทดสอบ Core Business Logic โดยไม่ต้องพึ่ง GraphQL
ข้อดีใหญ่ของ Hexagonal Architecture คือเราสามารถทดสอบ Use Case ได้โดยไม่ต้องมี GraphQL Server, Redis, หรือ PostgreSQL จริง
// typescript
// tests/unit/use-cases/place-order.use-case.spec.ts
import { PlaceOrderUseCase } from '../../../src/application/use-cases/place-order.use-case';
describe('PlaceOrderUseCase', () => {
let useCase: PlaceOrderUseCase;
let mockOrderRepo: jest.Mocked<IOrderRepository>;
let mockEventBus: jest.Mocked<IDomainEventBus>;
beforeEach(() => {
mockOrderRepo = {
save: jest.fn(),
findById: jest.fn()
} as any;
mockEventBus = {
publish: jest.fn(),
subscribe: jest.fn(),
unsubscribe: jest.fn()
} as any;
useCase = new PlaceOrderUseCase(mockOrderRepo, mockEventBus);
});
it('should save order and publish event', async () => {
const input = {
customerId: 'cust-1',
items: [{ productId: 'prod-1', quantity: 2, price: 100 }]
};
const result = await useCase.execute(input);
expect(mockOrderRepo.save).toHaveBeenCalled();
expect(mockEventBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
eventType: 'ORDER_PLACED',
customerId: 'cust-1'
})
);
expect(result.totalAmount).toBe(200);
});
});
5.2 การทดสอบ GraphQL Adapter แยกส่วน
สำหรับ Integration Test ของ Adapter เราสามารถทดสอบโดยใช้ Mock Domain Event Bus
// typescript
// tests/integration/graphql/subscriptions/order-subscription.spec.ts
import { execute } from 'graphql';
import { buildSchema } from 'graphql';
import { OrderSubscriptionResolver } from '../../../src/infrastructure/graphql/subscriptions/order.subscription';
describe('OrderSubscription Resolver', () => {
let resolver: OrderSubscriptionResolver;
let mockEventBus: jest.Mocked<IDomainEventBus>;
let mockSubscriptionService: jest.Mocked<ISubscriptionService>;
beforeEach(() => {
mockEventBus = { publish: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn() } as any;
mockSubscriptionService = { registerClient: jest.fn(), pushToTopic: jest.fn() } as any;
resolver = new OrderSubscriptionResolver(mockEventBus, mockSubscriptionService);
});
it('should register client when subscribing', async () => {
const context = { clientId: 'test-client', pubsub: createMockPubSub() };
const args = { orderId: 'order-123' };
// Simulate subscription registration
const iterator = resolver.resolvers.Subscription.orderUpdated.subscribe(
null, args, context
);
expect(mockSubscriptionService.registerClient).toHaveBeenCalledWith(
'test-client',
['order.order-123']
);
});
});
5.3 Monitoring และ Observability
สำหรับ Production เราควรมีระบบ Monitoring สำหรับ Subscription โดยเฉพาะ
| Metric | คำอธิบาย | เครื่องมือที่แนะนำ |
|---|---|---|
| Active Connections | จำนวน WebSocket ที่เชื่อมต่ออยู่ | Prometheus + Grafana |
| Events per Second | จำนวน Domain Events ที่ถูก publish ต่อวินาที | Redis Monitor |
| Subscription Latency | เวลาตั้งแต่ Event เกิดขึ้นจนถึง Client ได้รับ | OpenTelemetry |
| Error Rate | สัดส่วนการส่ง Subscription ที่ล้มเหลว | Datadog / New Relic |
| Topic Distribution | จำนวน Subscriber ต่อ Topic | Custom Logger |
6. Best Practices และ Real-World Use Cases
6.1 Best Practices สำหรับ Production
- ใช้ Backpressure Mechanism: เมื่อ Client รับข้อมูลไม่ทัน ควรมีกลไกจัดการ เช่น การ Drop Event หรือ Buffer แบบจำกัด
- Connection Pooling สำหรับ Redis: อย่าสร้าง Redis Connection ใหม่ทุกครั้ง ใช้ Pool แทน
- Graceful Shutdown: เมื่อเซิร์ฟเวอร์ปิดตัว ต้องแจ้ง Client ทุกตัวก่อนปิด WebSocket
- Rate Limiting: จำกัดจำนวน Subscription ต่อ Client เพื่อป้องกัน Resource Exhaustion
- Schema Stitching: ถ้ามีหลาย Microservice ควรใช้ Apollo Federation หรือ GraphQL Mesh
- Document Schema: ใช้ GraphQL SDL Comments หรือ Tool เช่น GraphQL Voyager
6.2 Real-World Use Case: ระบบติดตามสถานะการจัดส่ง
บริษัทโลจิสติกส์แห่งหนึ่งใช้ระบบนี้ในการติดตามพัสดุแบบ Real-time:
- Core Domain: จัดการสถานะพัสดุ (Parcel Status) และเหตุการณ์ต่างๆ (ParcelPickedUp, InTransit, OutForDelivery, Delivered)
- Ports: IParcelRepository, IDeliveryTrackingService, INotificationService
- Adapters:
- PostgreSQL สำหรับเก็บข้อมูลพัสดุ
- Redis Pub/Sub สำหรับกระจาย Event ระหว่าง Server Instance
- GraphQL Subscription สำหรับ Push ไปยัง Mobile App
- Firebase Cloud Messaging สำหรับ Push Notification (เพิ่มเติม)
- ผลลัพธ์:
- Latency ลดลงจาก 5 วินาที (Polling) เหลือน้อยกว่า 200ms
- ลดภาระเซิร์ฟเวอร์ลง 70% เพราะไม่ต้อง Poll ทุก 3 วินาที
- เปลี่ยนจาก REST เป็น GraphQL ได้โดยไม่กระทบ Business Logic
6.3 การเปรียบเทียบ: Hexagonal vs Traditional Architecture
| ลักษณะ | Traditional (Monolith + Direct Integration) | Hexagonal Architecture + GraphQL Subscriptions |
|---|---|---|
| การพึ่งพา Framework | สูง — Business Logic ผูกกับ Express/Apollo | ต่ำ — Logic อยู่ใน Pure TypeScript/Java |
| ความสามารถในการทดสอบ | ยาก — ต้อง Mock GraphQL Schema | ง่าย — ทดสอบ Use Case โดยตรง |
| การเปลี่ยนเทคโนโลยี | ยาก — ต้องแก้ Business Logic | ง่าย — เปลี่ยน Adapter เท่านั้น |
| Performance (Real-time) | ปานกลาง — Coupling สูง | ดีเยี่ยม — แยกส่วนชัดเจน, Scale ได้ |
| ความซับซ้อนเริ่มต้น | ต่ำ — เริ่มต้นง่าย | สูง — ต้องออกแบบ Ports/Adapters |
| การบำรุงรักษาระยะยาว | ยาก — โค้ดพึ่งพากันสูง | ง่าย — แต่ละส่วนแยกจากกัน |
6.4 ข้อควรระวังและคำแนะนำ
- อย่า Over-Engineer: ถ้าแอปพลิเคชันเล็กมาก การใช้ Hexagonal Architecture อาจเพิ่มความซับซ้อนโดยไม่จำเป็น
- เริ่มจาก Core ก่อน: อย่าเริ่มต้นด้วยการสร้าง Adapter ทุกตัว สร้าง Use Case และ Port ก่อน
- ใช้ Type Safety: TypeScript หรือภาษาที่มี Type System ช่วยลดข้อผิดพลาดในการเชื่อมต่อ Ports
- Logging ระดับ Event: บันทึกทุก Domain Event ที่ publish เพื่อการ Debug และ Audit
- Versioning Event: Domain Events ควรมี Version Number เพื่อรองรับ Schema Evolution
7. การ Scale และ Performance Optimization
7.1 การ Scale ในแนวนอน (Horizontal Scaling)
เมื่อต้องรองรับผู้ใช้งานหลายแสนคนพร้อมกัน การใช้ In-memory Pub/Sub (EventEmitter) จะไม่เพียงพอ เพราะ Event ไม่สามารถข้าม Instance ได้ วิธีแก้คือใช้ External Pub/Sub เช่น:
- Redis Pub/Sub: เหมาะสำหรับขนาดกลาง (ไม่เกิน 10,000 ข้อความ/วินาที)
- RabbitMQ / Kafka: สำหรับขนาดใหญ่ที่ต้องการ Persistent และ High Throughput
- Google Pub/Sub หรือ AWS SNS/SQS: สำหรับ Cloud-Native
7.2 การ Optimize WebSocket Connections
WebSocket แต่ละ Connection ใช้หน่วยความจำและ CPU ดังนั้นควร:
- ใช้ Load Balancer ที่รองรับ WebSocket Sticky Session (เช่น NGINX, HAProxy)
- กำหนด Connection Timeout และ Heartbeat Mechanism
- ใช้ GraphQL Batching สำหรับ Subscription ที่มีข้อมูลคล้ายกัน
7.3 การใช้ Event Sourcing ร่วมกับ Subscription
สำหรับระบบที่ต้องการ Audit Trail ครบถ้วน การใช้ Event Sourcing จะช่วยให้ Subscription สามารถ Replay Events ย้อนหลังได้:
// typescript
// src/infrastructure/eventstore/postgres-event-store.adapter.ts
export class PostgresEventStore implements IEventStore {
async save(event: DomainEvent): Promise<void> {
await this.db.query(
`INSERT INTO domain_events (id, aggregate_id, event_type, data, occurred_at)
VALUES ($1, $2, $3, $4, $5)`,
[event.id, event.aggregateId, event.eventType, JSON.stringify(event.data), event.occurredAt]
);
}
async getEventsByAggregateId(aggregateId: string): Promise<DomainEvent[]> {
const result = await this.db.query(
`SELECT * FROM domain_events WHERE aggregate_id = $1 ORDER BY occurred_at ASC`,
[aggregateId]
);
return result.rows.map(row => ({
id: row.id,
aggregateId: row.aggregate_id,
eventType: row.event_type,
data: row.data,
occurredAt: row.occurred_at
}));
}
}
8. สรุปและแนวโน้มในปี 2026
8.1 สิ่งที่เราได้เรียนรู้
การรวม GraphQL Subscriptions เข้ากับ Hexagonal Architecture ไม่ใช่เพียงแค่การเขียนโค้ดให้สวยงาม แต่เป็นกลยุทธ์ทางสถาปัตยกรรมที่ให้ประโยชน์หลายประการ:
- Separation of Concerns: Business Logic แยกจาก Infrastructure อย่างชัดเจน
- Testability: สามารถทดสอบแต่ละ Layer ได้อย่างอิสระ
- Flexibility: เปลี่ยนจาก GraphQL เป็น REST, gRPC, หรือ WebSocket Protocol อื่นได้โดยไม่กระทบ Core
- Scalability: รองรับการขยายระบบในแนวนอนได้ง่าย
- Maintainability: โค้ดอ่านง่าย แก้ไขตรงจุด
8.2 แนวโน้มในปี 2026
จากประสบการณ์ในวงการและแนวโน้มที่สังเกตได้:
- GraphQL Federation 2.0: การรวม Subscription ข้าม Microservice จะเป็นมาตรฐานมากขึ้น
- Edge Computing: การรัน Subscription Resolver ที่ Edge (Cloudflare Workers, Deno Deploy) เพื่อลด Latency
- WebTransport: Protocol ใหม่ที่เร็วกว่า WebSocket เริ่มถูกนำมาใช้ใน GraphQL
- AI-Powered Caching: การใช้ Machine Learning เพื่อทำนาย Event ที่ Client จะสมัครรับ ลดภาระเซิร์ฟเวอร์
- Zero-Downtime Deployment: เทคนิคการ Deploy ที่ไม่กระทบ Subscription ที่กำลังทำงาน
Summary
GraphQL Subscriptions บน Hexagonal Architecture เป็นแนวทางที่ทรงพลังสำหรับการสร้างระบบ Real-time ที่ยืดหยุ่น ทดสอบได้ และพร้อมขยายขนาดในปี 2026 การแยก Core Business Logic ออกจาก Infrastructure ผ่าน Ports และ Adapters ช่วยให้ทีมพัฒนาสามารถ:
- พัฒนาและทดสอบ Business Logic โดยไม่ต้องพึ่งพา GraphQL Server หรือ Database
- เปลี่ยนเทคโนโลยีที่อยู่ภายใต้ (เช่น จาก Redis เป็น Kafka) ได้โดยไม่ต้องแก้ไข Core Code
- เพิ่มความปลอดภัยด้วยการจัดการ Authentication/Authorization ใน Layer ที่เหมาะสม
- Monitor และ Debug ได้ง่ายขึ้นด้วย Domain Events ที่มีโครงสร้างชัดเจน
แม้การเริ่มต้นด้วย Hexagonal Architecture อาจรู้สึกยุ่งยากกว่าการเขียน GraphQL Resolver แบบตรงไปตรงมา แต่ในระยะยาว โดยเฉพาะเมื่อโปรเจกต์เติบโตและต้องรองรับ Feature ใหม่ๆ ข้อดีที่ได้จะคุ้มค่ากับการลงทุนในช่วงแรกอย่างแน่นอน
สำหรับทีมที่กำลังวางแผนสร้างระบบ Real-time ใหม่ หรือต้องการ Refactor ระบบเดิม ขอแนะนำให้เริ่มต้นด้วยการออกแบบ Core Domain Events และ Ports ก่อน แล้วค่อยสร้าง Adapter สำหรับ GraphQL Subscriptions ทีละชั้น วิธีนี้จะช่วยให้คุณมีระบบที่แข็งแกร่ง พร้อมรับทุกความท้าทายในโลกการพัฒนาเว็บยุค 2026
บทความนี้เขียนโดยทีม SiamCafe Blog — แหล่งความรู้ด้านเทคโนโลยีสำหรับนักพัฒนาไทย 如果您มีข้อสงสัยหรือต้องการแลกเปลี่ยนประสบการณ์ สามารถติดต่อได้ที่ forum.siamcafe.dev