Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Ngày xuất bản
Ngày xuất bản

Timezone Và Datetime Trong Backend: Dự Án Nhật vs Global

Timezone là một trong số ít chủ đề mà gần như Developer nào cũng nghĩ mình đã hiểu rõ — cho đến khi gặp một bug kiểu: "sao báo cáo hôm qua lại thiếu mất mấy giờ đầu ngày?", hoặc "sao 2 người query cùng 1 record lại ra 2 giá trị giờ khác nhau?". Đây không phải bug logic thông thường, mà là hệ quả của việc nhầm lẫn giữa nhiều khái niệm tưởng chừng giống nhau: kiểu cột DATETIME với TIMESTAMP, timezone của database với timezone của application, và quy tắc "luôn lưu UTC" — một best practice rất phổ biến nhưng không phải lúc nào cũng đúng.

Bài viết này tổng hợp lại toàn bộ kiến thức nền về timezone/datetime trong backend, dựa trên một tình huống thực tế đã xảy ra khi chuẩn hoá timezone cho một hệ thống MySQL + Laravel phục vụ khách hàng Nhật Bản, đồng thời đối chiếu với chiến lược cần dùng khi làm dự án global. Sau khi đọc xong, bạn sẽ trả lời được: khi nào nên dùng DATETIME thay vì TIMESTAMP, vì sao set time_zone cho DB vẫn cần thiết ngay cả khi cột đã là DATETIME, APP_TIMEZONE nên để UTC hay theo múi giờ khách hàng, và quan trọng nhất — chiến lược nào đúng cho dự án chỉ phục vụ một quốc gia (single-timezone) so với dự án phục vụ nhiều quốc gia (multi-timezone).

Vì Sao Timezone Lại Là Nguồn Gốc Của Nhiều Bug Âm Thầm?

Bug về timezone nguy hiểm hơn nhiều loại bug khác vì nó không throw exception, không crash — nó chỉ âm thầm cho ra sai số liệu. Một vài triệu chứng điển hình:

  • Báo cáo "doanh thu hôm qua" bị lệch vì hệ thống tính "hôm qua" theo UTC trong khi người dùng đang ở JST (UTC+9).
  • Cùng một record, đọc qua tool A (kết nối timezone UTC) ra một giờ, đọc qua tool B (kết nối timezone JST) ra giờ khác — dù dữ liệu trong DB không hề thay đổi.
  • Batch job chạy INSERT ... VALUES (NOW()) cho ra timestamp lệch 9 tiếng so với đồng hồ thực tế của văn phòng.
  • Field ngày hết hạn hiển thị đúng ở local (dev) nhưng sai ở production vì server có timezone khác máy dev.

Gốc rễ của toàn bộ nhóm lỗi này nằm ở việc lẫn lộn 3 khái niệm hoàn toàn độc lập: kiểu dữ liệu lưu trữ (DATETIME hay TIMESTAMP), timezone của tầng database (session/global time_zone), và timezone của tầng application (APP_TIMEZONE, date_default_timezone_set). Hiểu rõ ranh giới giữa 3 tầng này là điều kiện cần để thiết kế đúng ngay từ đầu.

Kiến Thức Nền: DATETIME vs TIMESTAMP Trong MySQL

Đây là điểm nhầm lẫn phổ biến nhất. Cả hai đều lưu ngày-giờ, nhưng cơ chế hoàn toàn khác nhau:

Tính năng DATETIME TIMESTAMP
Cách lưu Lưu literal — đúng y hệt giá trị được insert, không convert Lưu dạng UTC nội bộ, tự convert theo timezone của session khi đọc/ghi
Ảnh hưởng khi đổi time_zone Không đổi — đọc ra luôn giống lúc ghi vào Giá trị đọc ra thay đổi tuỳ theo time_zone hiện tại của session
Range 1000-01-01 đến 9999-12-31 1970-01-01 đến 2038-01-19 (giới hạn 32-bit)
Phù hợp khi Hệ thống chỉ phục vụ 1 timezone, muốn giá trị "WYSIWYG" (ghi sao đọc vậy) Cần tự động quy đổi hiển thị theo timezone của từng client/session

Vấn đề thực tế: nếu cột dùng TIMESTAMP và server đổi time_zone (ví dụ do migrate hạ tầng, đổi region RDS), toàn bộ giá trị cũ tự động bị đọc ra khác đi — dù không ai UPDATE gì cả. Đây chính là loại bug "đọc cùng 1 record ra 2 giá trị khác nhau tùy session/tool đang query". Đổi cột sang DATETIME giải quyết đúng vấn đề này: giá trị literal cố định, ai đọc cũng ra y hệt, không phụ thuộc session đang connect với time_zone nào.

Trước khi sửa từng cột, nên audit toàn bộ database để biết chính xác các audit field (created_at, updated_at, deleted_at) hiện đang là kiểu dữ liệu nào ở từng table:

SELECT
    TABLE_NAME,
    COLUMN_NAME,
    DATA_TYPE,
    COLUMN_TYPE,
    IS_NULLABLE,
    COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND COLUMN_NAME IN ('created_at', 'updated_at', 'deleted_at')
ORDER BY
    TABLE_NAME,
    FIELD(COLUMN_NAME, 'created_at', 'updated_at', 'deleted_at');

Kết quả trả về DATA_TYPEtimestamp chính là danh sách các cột cần rà soát để đổi sang datetime (nếu hệ thống thuộc Use Case 1 — single-timezone). Cột nào có COLUMN_DEFAULTCURRENT_TIMESTAMP cũng cần lưu ý riêng, vì đây là default sinh giá trị trực tiếp từ MySQL — phụ thuộc time_zone của DB, không đi qua application code.

-- Đổi cột từ TIMESTAMP sang DATETIME để tránh auto-convert theo session timezone
ALTER TABLE users
  MODIFY COLUMN created_at DATETIME NOT NULL,
  MODIFY COLUMN updated_at DATETIME NULL;

DB Timezone (time_zone): Đổi Sang DATETIME Rồi Có Còn Cần Set Không?

Câu trả lời là vẫn cần, nhưng lý do đã khác hoàn toàn so với ban đầu. Đây là điểm rất dễ hiểu nhầm: nhiều Developer nghĩ rằng một khi cột đã là DATETIME (không còn tự convert), thì time_zone của DB không còn ý nghĩa gì nữa. Thực tế, DATETIME và time_zone giải quyết hai vấn đề độc lập nhau:

Vấn đề Nguyên nhân DATETIME có giải quyết? Set time_zone DB có giải quyết?
#1 — Đọc cùng 1 record ra 2 giá trị khác nhau tùy session/tool Kiểu cột TIMESTAMP tự convert ✅ Có Không liên quan
#2 — NOW() / CURDATE() trả sai "hôm nay" khi report/filter dùng SQL trực tiếp Session/global time_zone của DB ❌ Không ✅ Có

Vấn đề #2 là loại lỗi rất dễ bị bỏ sót vì nó không nằm ở dữ liệu đã lưu, mà nằm ở các hàm thời gian của chính DB khi được gọi trực tiếp trong SQL thô — ví dụ một tool báo cáo nội bộ do một phòng ban tự viết để tổng hợp dữ liệu "hôm qua":

-- Lấy dữ liệu "hôm qua"
SELECT * FROM users WHERE DATE(created_at) = CURDATE() - INTERVAL 1 DAY;

-- Lấy dữ liệu "hôm nay"
SELECT * FROM users WHERE created_at >= CURDATE();

Trước khi kết luận nguyên nhân, luôn kiểm tra lại timezone hiện tại của DB bằng 3 lệnh sau — đây cũng là bộ lệnh nên chạy đầu tiên mỗi khi nghi ngờ có bug liên quan đến "hôm nay/hôm qua":

-- Timezone của session hiện tại (kết nối đang dùng)
SELECT @@session.time_zone;

-- Timezone global của toàn server MySQL
SELECT @@global.time_zone;

-- Giá trị "bây giờ" mà DB đang hiểu, theo đúng timezone ở trên
SELECT NOW();

created_at giờ là DATETIME chứa literal đúng giờ Nhật (JST) — nhưng nếu time_zone của DB vẫn là UTC thì CURDATE() trả về ngày UTC hiện tại, lệch 9 tiếng so với ngày JST thật. Kết quả: đúng y hệt triệu chứng ban đầu ("sai hôm qua/hôm nay"), chỉ khác là lỗi nằm ở phía "DB hiểu sai bây giờ là mấy giờ", chứ không phải "record bị lưu sai". Tương tự, các cột dùng default DB-side (ví dụ $table->timestamp('updated_at')->useCurrent() trong Laravel migration) hoặc bất kỳ INSERT ... VALUES (NOW()) / batch SQL thô nào cũng lấy giá trị trực tiếp từ MySQL — không đi qua PHP — nên vẫn cần time_zone=Asia/Tokyo để tự sinh ra literal đúng ngay từ đầu.

-- Set timezone cho session hiện tại
SET time_zone = '+09:00';

-- Hoặc set global (cần quyền SUPER, hoặc cấu hình ở RDS Parameter Group)
SET GLOBAL time_zone = 'Asia/Tokyo';

Kết luận cho phần này: DATETIME giải quyết vấn đề "column-type tự convert", còn time_zone DB giải quyết vấn đề "DB hiểu đúng bây giờ là mấy giờ" để các hàm NOW()/CURDATE()/CURRENT_TIMESTAMP cho kết quả đúng khi kết hợp với dữ liệu literal. Giữ đúng cả hai — không phải cái này thay thế được cái kia.

Tầng Application: APP_TIMEZONE Nên Để UTC Hay Theo Local?

Đây là câu hỏi kinh điển thứ hai, và câu trả lời phụ thuộc hoàn toàn vào phạm vi phục vụ của dự án — đây chính là chỗ mà hai use case (Nhật Bản vs global) rẽ nhánh hoàn toàn khác nhau.

Quy tắc phổ biến nhất mà hầu hết tài liệu best practice đều nhắc tới là: "luôn lưu và xử lý nội bộ bằng UTC, chỉ convert sang local timezone lúc hiển thị cho người dùng." Đây là quy tắc đúng — nhưng nó được sinh ra để giải quyết một vấn đề cụ thể: hệ thống phục vụ nhiều timezone khác nhau, và/hoặc timezone đó có DST (Daylight Saving Time — giờ mùa hè). Nếu dự án của bạn không rơi vào tình huống đó, áp dụng máy móc quy tắc này có thể tạo ra rủi ro mới thay vì loại bỏ rủi ro cũ.

Use Case 1: Dự Án Chỉ Phục Vụ Nhật Bản (Single-Timezone, No DST)

Đây là tình huống thực tế đã xảy ra với một hệ thống quản lý kho vận (WMS) viết bằng Laravel, chỉ phục vụ khách hàng và vận hành tại Nhật Bản, chạy trên RDS MySQL. Sau khi chuẩn hoá cột sang DATETIME và set time_zone=Asia/Tokyo cho DB, câu hỏi tiếp theo là: APP_TIMEZONE trong Laravel nên để UTC (theo "best practice") hay Asia/Tokyo?

Quyết định cuối cùng: giữ APP_TIMEZONE=Asia/Tokyo, không chuyển về UTC. Lý do:

  • DST không tồn tại ở Nhật Bản. Vấn đề mà quy tắc "luôn dùng UTC nội bộ" giải quyết — ambiguous time khi đồng hồ chuyển mùa (ví dụ 2:30 AM xảy ra 2 lần trong ngày chuyển DST) — đơn giản là không xảy ra ở Nhật.
  • Không có nhiều timezone cần quy đổi. Toàn bộ user, server, và dữ liệu đều nằm trong một timezone duy nhất. Không có khái niệm "user A ở Tokyo xem giờ khác user B ở New York".
  • Tránh lỗi cộng/trừ tay. Nếu để APP_TIMEZONE=UTC trong khi DB lưu literal giờ Nhật, mọi chỗ code gọi now() / Carbon::now() trước khi ghi vào cột đó đều phải tự cộng tay +9 giờ — chỉ cần một chỗ quên cộng là dữ liệu sai, đúng loại lỗi mà cả hệ thống đang cố loại bỏ. Để APP_TIMEZONE=Asia/Tokyo cho phép "WYSIWYG": now() trong PHP = giá trị nằm trong DB = giờ Nhật thật, không cần convert ở bất kỳ đâu.
  • Đồng bộ tự động cho log, Scheduler, và mọi cron job.config/app.php được load bởi LoadConfiguration gọi date_default_timezone_set(), một khi APP_TIMEZONE=Asia/Tokyo thì log Laravel, now(), và toàn bộ $schedule->... trong Scheduler đều tự động chạy theo JST — không cần set riêng lẻ timezone cho từng job bằng các biến môi trường rời rạc.
// config/app.php
'timezone' => env('APP_TIMEZONE', 'Asia/Tokyo'),
// Trước đây: phải set timezone riêng cho từng scheduled job
$schedule->command('wms:sync-stock')
    ->dailyAt('01:00')
    ->timezone('Asia/Tokyo'); // dư thừa nếu APP_TIMEZONE đã là Asia/Tokyo

// Sau khi APP_TIMEZONE=Asia/Tokyo: không cần khai báo timezone() nữa
$schedule->command('wms:sync-stock')->dailyAt('01:00');

Tương tự như kiểm tra timezone ở tầng DB, có thể xác nhận nhanh Laravel đang thực sự chạy theo timezone nào bằng tinker:

php artisan tinker
config('app.timezone');
date_default_timezone_get();
now();

Nếu APP_TIMEZONE=Asia/Tokyo đã được set và cache config đã clear (php artisan config:clear sau khi đổi .env), cả 3 lệnh trên phải đồng nhất: config('app.timezone') trả về Asia/Tokyo, date_default_timezone_get() cũng trả về Asia/Tokyo (xác nhận date_default_timezone_set() đã được gọi đúng), và now() phải hiển thị đúng giờ Nhật hiện tại — không lệch 9 giờ so với đồng hồ thực tế.

Ví Dụ Thực Tế: Báo Cáo Triển Khai Cho Team

Dưới đây là một ví dụ báo cáo triển khai thật, khi khách hàng yêu cầu thiết lập giờ Nhật ở database vì lý do nghiệp vụ:

Khách hàng có yêu cầu thiết lập giờ Nhật ở database vì nghiệp vụ. Đã hoàn thành các công việc sau:

  • ✅ Chuyển đổi tất cả các table columns từ timestamp sang datetime
  • ✅ Set timezone (Database, Laravel) sang Asia/Tokyo
  • ✅ [Test] Tạo user và xem audit field value theo giờ Nhật

Môi trường áp dụng: dev + demo

Impact: mỗi khi tạo/xoá record → giá trị created_at, updated_at, deleted_at sẽ theo giờ Nhật.

Cụ thể hoá impact bằng một ví dụ số liệu — giả sử thời điểm thực tế đang là 18/09/2026 15:00 (giờ Việt Nam, UTC+7):

Tầng Giá trị
Thời điểm thực tế (giờ VN, UTC+7) 2026-09-18 15:00
Giá trị lưu trong DB (created_at, literal JST, UTC+9) 2026-09-18 17:00:00
API trả về Frontend (ISO 8601, UTC) 2026-09-18T08:00:00.000000Z
UI hiển thị nếu user ở Nhật 17:00
UI hiển thị nếu user ở Việt Nam 15:00

Có 3 điểm cần lưu ý từ ví dụ này:

  • DB luôn lưu literal JST (17:00:00) — đúng chiến lược "local-native" đã trình bày ở trên: now(), current date ở PHP, Carbon, và MySQL đều trả về giờ Nhật, không cần convert tay ở bất kỳ đâu trong application code.
  • Serialize ra API vẫn là UTC (...Z) — đây là hành vi mặc định của Carbon khi serialize sang JSON (dùng chuẩn ISO 8601 UTC), độc lập với APP_TIMEZONE. Điều này không mâu thuẫn với chiến lược local-native: DB/application vẫn xử lý JST end-to-end, chỉ riêng bước serialize ra API là quy về UTC theo chuẩn ISO 8601 — một chuẩn giao tiếp trung lập, để Frontend tự convert sang timezone của người xem.
  • UI hiển thị theo local của người dùng — Frontend nhận UTC, rồi convert sang timezone của trình duyệt/thiết bị: cùng một record hiển thị 17:00 nếu người xem ở Nhật, nhưng 15:00 nếu người xem ở Việt Nam. Đây chính là lý do bước serialize ra UTC ở tầng API vẫn hữu ích, dù DB/backend nội bộ đã dùng JST — nó cho phép UI đúng luôn với bất kỳ ai xem, kể cả khi team dev/QC đang ở Việt Nam theo dõi một hệ thống chỉ phục vụ khách hàng Nhật.

Tóm lại, với dự án chỉ phục vụ một quốc gia, một timezone, không DST: chiến lược đúng là "local-native" — DB literal theo local time, application timezone khớp local time, không có bước convert nào ở giữa. Đây cũng là quy ước phù hợp với các hệ thống legacy on-premise của Nhật (kiểu hệ thống PDB/SPS/LIS truyền thống) vốn chạy JST end-to-end và không có khái niệm UTC ở bất kỳ tầng nào.

Use Case 2: Dự Án Global / Đa Timezone (Multi-Region, Có DST)

Ngược lại hoàn toàn, nếu hệ thống phục vụ người dùng ở nhiều quốc gia — ví dụ một SaaS B2B có khách hàng ở Nhật, Mỹ, và châu Âu — thì chiến lược "local-native" ở trên sẽ sụp đổ ngay lập tức, vì không còn khái niệm "một local time duy nhất" nữa. Lúc này, quy tắc kinh điển "luôn lưu và tính toán nội bộ bằng UTC, chỉ convert khi hiển thị" mới thực sự phát huy tác dụng.

// config/app.php — dự án global
'timezone' => env('APP_TIMEZONE', 'UTC'),
-- Migration cho dự án global: dùng TIMESTAMP hoặc DATETIME lưu UTC,
-- convert sang timezone của user chỉ ở tầng presentation
CREATE TABLE orders (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  created_at DATETIME NOT NULL COMMENT 'Always stored in UTC',
  user_timezone VARCHAR(64) NOT NULL COMMENT 'e.g. Asia/Tokyo, America/New_York'
);

Điểm khác biệt cốt lõi so với Use Case 1: hệ thống global phải lưu kèm timezone của user (hoặc suy ra từ profile/request), vì cùng một UTC timestamp phải hiển thị khác nhau tuỳ người xem.

use Carbon\Carbon;

// Lưu: luôn ép về UTC trước khi ghi DB
$order->created_at = Carbon::now('UTC');

// Hiển thị: convert UTC -> timezone của user tại thời điểm render
$displayTime = Carbon::parse($order->created_at, 'UTC')
    ->setTimezone($user->timezone) // ví dụ 'America/New_York'
    ->format('Y-m-d H:i:s');

Ở tầng frontend/API, cách an toàn nhất là API luôn trả về datetime theo chuẩn ISO 8601 kèm offset UTC (2026-09-18T01:00:00Z), để client tự convert theo timezone của trình duyệt hoặc lựa chọn của user:

// Frontend: convert UTC ISO string sang local time của trình duyệt
const utcString = "2026-09-18T01:00:00Z";
const localTime = new Intl.DateTimeFormat("ja-JP", {
  dateStyle: "medium",
  timeStyle: "short",
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}).format(new Date(utcString));

Với dự án global, còn cần xử lý thêm các vấn đề mà dự án single-timezone không bao giờ gặp: DST (một số quốc gia như Mỹ, châu Âu đổi giờ 2 lần/năm, khiến offset UTC của cùng một timezone thay đổi theo mùa), ambiguous/invalid local time khi convert ngược từ local sang UTC đúng vào thời điểm chuyển DST, và lịch trình cron job phải tính theo UTC thay vì giờ địa phương để tránh bị lệch khi user ở các timezone khác nhau cùng theo dõi một job.

So Sánh Hai Chiến Lược

Tiêu chí Dự án chỉ phục vụ Nhật Bản Dự án global / đa timezone
APP_TIMEZONE Local (Asia/Tokyo) UTC
DB time_zone Local (Asia/Tokyo) UTC (hoặc không quan trọng nếu dùng DATETIME thuần)
Giá trị lưu trong cột datetime Local time literal (WYSIWYG) UTC, kèm cột lưu timezone của user nếu cần hiển thị lại đúng local
Cần xử lý DST? Không
Scheduler / cron Chạy theo local time trực tiếp Nên định nghĩa theo UTC, convert khi cần hiển thị lịch cho user
Rủi ro chính nếu làm sai DB hiểu sai "hôm nay" khi query SQL thô (NOW()/CURDATE()) Quên convert ở một điểm ghi/đọc dữ liệu, gây lệch giờ ẩn giữa các timezone
Độ phức tạp Thấp — không cần convert ở đâu cả Cao hơn — cần convert nhất quán ở mọi điểm vào/ra dữ liệu

Bài học quan trọng nhất rút ra từ việc so sánh hai use case: "luôn dùng UTC nội bộ" là best practice cho hệ đa timezone, không phải một luật bất biến áp dụng cho mọi hệ thống. Áp dụng đúng ngữ cảnh của dự án quan trọng hơn là áp dụng máy móc một quy tắc nghe có vẻ "chuẩn".

Checklist Triển Khai Timezone Đúng Ngay Từ Đầu

  • [ ] Xác định rõ dự án là single-timezone hay multi-timezone trước khi thiết kế schema — đổi chiến lược giữa chừng rất tốn kém.
  • [ ] Với single-timezone (ví dụ chỉ phục vụ Nhật): dùng DATETIME, set time_zone của DB khớp local, APP_TIMEZONE khớp local — tất cả đồng bộ một chiều.
  • [ ] Với multi-timezone: luôn lưu UTC ở tầng DB và application, lưu kèm timezone của user khi cần hiển thị lại đúng local time.
  • [ ] Kiểm tra tất cả các nguồn sinh ra datetime không đi qua application code: default DB-side (useCurrent(), ON UPDATE CURRENT_TIMESTAMP), batch SQL thô, stored procedure — những chỗ này phụ thuộc trực tiếp vào time_zone của DB, không phụ thuộc APP_TIMEZONE.
  • [ ] Với mọi tool báo cáo/filter viết SQL trực tiếp dùng NOW()/CURDATE(), xác nhận DB đang hiểu đúng "bây giờ" theo timezone mà nghiệp vụ mong đợi.
  • [ ] Ghi lại quyết định timezone vào tài liệu kiến trúc (ADR) của dự án — đây là loại quyết định dễ bị người sau vô tình "sửa cho đúng best practice" rồi gây bug ngược, nếu không có lý do được ghi lại rõ ràng.

Các Lỗi Thường Gặp Và Cách Debug

  • Đọc 2 giá trị khác nhau cho cùng 1 record: kiểm tra kiểu cột — nếu là TIMESTAMP, giá trị hiển thị phụ thuộc time_zone của session đang kết nối. Cân nhắc đổi sang DATETIME nếu hệ thống không cần auto-convert.
  • Report "hôm qua/hôm nay" sai: chạy SELECT NOW(), @@session.time_zone, @@global.time_zone; để kiểm tra DB đang hiểu "bây giờ" là mấy giờ, theo timezone nào.
  • Giờ đúng ở local, sai ở production: kiểm tra timezone của OS/container production (date, biến môi trường TZ) có khớp với APP_TIMEZONE không — một số hệ điều hành container mặc định UTC dù code cấu hình Asia/Tokyo.
  • Cron job chạy sai giờ sau khi deploy lên server mới: với dự án global, xác nhận Scheduler đang định nghĩa lịch theo UTC chứ không phải giờ địa phương của server cũ.
  • Dữ liệu import từ Excel/CSV lệch giờ: kiểm tra file gốc có kèm thông tin timezone hay không — Excel thường lưu datetime dạng "naive" (không timezone), dễ bị hiểu nhầm là UTC khi import vào hệ thống global.

Kết Luận

Xử lý timezone và datetime đúng không nằm ở việc học thuộc một quy tắc duy nhất, mà nằm ở việc hiểu rõ ba tầng độc lập — kiểu cột lưu trữ, timezone của DB, và timezone của application — rồi chọn chiến lược phù hợp với phạm vi thực tế của dự án. Với dự án chỉ phục vụ một quốc gia như Nhật Bản, chiến lược "local-native" (DATETIME + time_zone local + APP_TIMEZONE local) đơn giản hơn, ít điểm dễ sai hơn, và đúng bản chất bài toán hơn so với việc áp dụng máy móc UTC. Với dự án global, quy tắc "luôn UTC nội bộ, convert khi hiển thị" là bắt buộc để tránh lệch giờ giữa các region và xử lý đúng DST. Điều quan trọng nhất là quyết định này cần được đưa ra có chủ đích, ghi lại lý do, và áp dụng nhất quán ở mọi điểm ghi/đọc dữ liệu — vì đây là loại bug chỉ lộ ra khi đã có dữ liệu thật, và rất tốn kém để sửa ngược sau khi hệ thống đã lên production.

Tham Khảo