Khi một hệ thống chỉ phục vụ người dùng trong cùng một khu vực địa lý, time zone thường chưa gây ra nhiều vấn đề. Developer, user, server và business có thể vô tình cùng nằm trong một múi giờ, khiến code tưởng như luôn đúng.
Nhưng khi hệ thống bắt đầu phục vụ khách hàng toàn cầu, có team remote xuyên quốc gia, hoặc có nhiều office/market ở các quốc gia khác nhau, time zone trở thành một phần của domain design.
Một hệ thống thực tế có thể đồng thời có:
Customer → Tokyo
Operations team → London
Developer → Vietnam
Office A → New York
Office B → Los Angeles
Server → Singapore
Database → UTCLúc này câu:
“Sự kiện diễn ra lúc 9 giờ sáng”
Không còn đủ thông tin.
Câu hỏi bắt buộc phải là:
9 giờ sáng ở đâu?
Đó là điểm bắt đầu của mọi bài toán timezone.
1. Time zone là một phần của domain design
Một cách tiếp cận khá phổ biến là coi timezone đơn giản như vấn đề presentation:
Backend lưu date
Frontend format theo timezone
Cách nghĩ này chỉ đúng một phần.
Timezone có thể ảnh hưởng trực tiếp đến:
Business hours
Calendar date
Daily report
Recurring schedule
Notification
Deadline
Payment timestamp
User activity
Cron job
Analytics
Monthly KPI
Remote collaboration
Ví dụ một công ty ở New York tổ chức webinar:
September 15
10:00 AM
America/New_YorkSpeaker ở New York, nhưng người tham gia có thể ở:
London
Tokyo
Vietnam
Sydney
SingaporeInfrastructure lại có thể chạy tại Singapore.
Cùng một webinar nhưng mỗi người sẽ nhìn thấy một giờ khác nhau. Tuy nhiên, tất cả đều đang nói về cùng một thời điểm tuyệt đối.
Để thiết kế hệ thống timezone đúng, trước hết cần phân biệt ba loại dữ liệu thời gian.
Absolute time — một thời điểm cụ thể
Ví dụ:
2026-09-15T14:00:00ZĐây là một instant.
Nó đại diện cho đúng một thời điểm trên timeline toàn cầu.
Cùng instant đó có thể được hiển thị:
New York → 10:00
London → 15:00
Vietnam → 21:00
Tokyo → 23:00Nhưng underlying instant không thay đổi.
Các field thường thuộc nhóm này:
created_at
updated_at
published_at
paid_at
logged_in_at
event_start_at
event_end_at
notification_sent_atNếu câu hỏi là:
“Sự kiện thực sự xảy ra lúc nào?”
thì chúng ta đang nói về absolute time.
Local business time — thời gian mang ý nghĩa địa phương
Giả sử một office hoạt động:
Monday
09:00 → 18:0009:00 ở đây chưa phải một instant.
Nó có nghĩa:
Văn phòng mở cửa lúc 9 giờ sáng theo giờ địa phương.
Semantic đầy đủ phải là:
Monday
09:00
America/New_YorkKhông nên convert recurring business rule này sang UTC rồi lưu cố định.
Ví dụ:
09:00 America/New_Yorkcó lúc tương ứng với:
13:00 UTCnhưng có lúc lại là:
14:00 UTCDo DST.
Vì vậy local business time nên giữ dưới dạng local wall-clock time + timezone.
Duration — khoảng thời gian
Ví dụ:
Meeting duration = 90 minutes
Session timeout = 30 minutes
Trial period = 14 daysDuration không có timezone.
90 minutesở New York vẫn là 90 phút ở Tokyo.
Nếu một field đơn giản chỉ biểu diễn elapsed duration thì không cần gắn timezone vào nó.
2. UTC và Business Timezone có hai vai trò khác nhau
Một convention rất hữu ích cho distributed systems là:
Absolute time nên được normalize về UTC trong core system.
Quay lại webinar:
September 15
10:00
America/New_YorkBackend có thể resolve thành:
2026-09-15T14:00:00Z
Từ đó core system chỉ cần làm việc với instant này:
Database
event_start_at = 2026-09-15T14:00:00Z
Queue
execute_at = 2026-09-15T14:00:00Z
Notification
send_at = 2026-09-15T13:30:00Z
Analytics
timestamp = 2026-09-15T14:00:00ZĐiểm mạnh của UTC là toàn bộ hệ thống có cùng một timeline.
Không quan trọng:
server đang ở đâu
developer đang ở đâu
user đang ở đâu
worker chạy ở region nàoMột instant vẫn là một instant.
Tuy nhiên, câu:
“Cứ lưu tất cả bằng UTC là hết timezone bug.”
không hoàn toàn đúng.
UTC rất tốt để trả lời:
Sự kiện xảy ra vào thời điểm nào?
Nhưng business thường hỏi:
Sự kiện thuộc ngày nào?
Hai câu hỏi này khác nhau.
Giả sử một transaction xảy ra:
2026-09-01T03:30:00ZTại London:
September 1
04:30Tại Los Angeles:
August 31
20:30Nếu manager tại Los Angeles hỏi:
Revenue ngày August 31 là bao nhiêu?
thì query theo UTC day:
August 31 00:00 UTC → September 1 00:00 UTCsẽ không đại diện chính xác business day tại Los Angeles.
Đúng hơn phải là:
August 31 00:00 America/Los_Angeles → resolve UTCSeptember 1 00:00 America/Los_Angeles → resolve UTCSau đó mới query database bằng UTC range.
Mental model:
Business date
↓
Business timezone
↓
Resolve UTC boundaries
↓
Query UTC dataServer timezone không phải business timezone
Giả sử:
Developer → Vietnam
Server → Singapore
Business → New York
Code:
const now = new Date();
không có vấn đề nếu mục tiêu chỉ là lấy instant hiện tại.
Nhưng code:
const today = dayjs();
const day = today.format('dddd');
có thể sai nếu business question thực sự là:
Hôm nay ở New York là thứ mấy?
Ví dụ:
Singapore → Tuesday 01:00
New York → Monday 13:00Server đã sang Tuesday nhưng business vẫn còn Monday.
Nếu lấy working schedule bằng server local date thì business logic có thể chọn sai ngày.
Vì vậy server timezone không nên vô tình tham gia vào domain decision.
Setting server thành UTC vẫn là một convention tốt:
Application runtime → UTC
Database → UTC
Container → UTC
Scheduler → UTCnhưng business logic vẫn phải explicit timezone.
Browser timezone cũng không phải source of truth
Frontend thường có thể biết timezone của user thông qua:
Intl.DateTimeFormat().resolvedOptions().timeZoneVí dụ:
Asia/Ho_Chi_MinhĐiều này rất hữu ích cho presentation.
Nhưng browser timezone không phải lúc nào cũng là business timezone.
Ví dụ một manager đang đi công tác Tokyo nhưng quản lý office London:
Browser timezone → Asia/Tokyo
Business timezone → Europe/LondonNếu dashboard sử dụng browser timezone để xác định:
Today
Yesterday
This week
Current monththì report có thể sai.
Trước mỗi UI nên xác định rõ:
Thời gian này phải hiển thị theo viewer timezone hay business timezone?
Ví dụ:
Messaging app thường ưu tiên viewer timezone.
Operational dashboard thường ưu tiên business timezone.
3. Model dữ liệu thời gian đúng semantic
Sau khi phân biệt instant và local business time, bước tiếp theo là model dữ liệu sao cho semantic không bị mất.
Dùng IANA timezone thay vì fixed offset
Không nên model timezone chỉ bằng:
UTC+7
UTC-5
GMT+1Thay vào đó nên sử dụng IANA timezone identifier:
Asia/Ho_Chi_Minh
America/New_York
America/Los_Angeles
Europe/London
Asia/TokyoLý do lớn nhất là DST.
Ví dụ:
America/New_Yorkkhông phải lúc nào cũng là:
UTC-5Ở một số thời điểm nó có thể là:
UTC-4Nếu hệ thống chỉ lưu:
offset = -5thì nó không đủ thông tin để xác định timezone behavior trong tương lai.
Có thể hiểu đơn giản:
Offset = kết quả tại một thời điểm
Timezone = rule dùng để tính offsetVì vậy source of truth nên là timezone identifier, không phải offset.
Absolute timestamp trong Database
Với PostgreSQL, absolute timestamp thường phù hợp với:
TIMESTAMPTZVí dụ:
CREATE TABLE webinar_events (
id UUID PRIMARY KEY,
organizer_timezone VARCHAR(100) NOT NULL,
start_at TIMESTAMPTZ NOT NULL,
end_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Một record conceptually có thể là:
organizer_timezone: America/New_York
start_at: 2026-09-15T14:00:00ZĐiểm quan trọng là TIMESTAMPTZ không có nghĩa PostgreSQL lưu:
America/New_Yorkbên trong timestamp.
Timestamp biểu diễn một instant.
Timezone có business meaning vẫn nên được lưu riêng.
Có thể hiểu:
start_at → WHEN
organizer_timezone → BUSINESS CONTEXTHai field phục vụ hai câu hỏi khác nhau.
API contract phải làm rõ semantic
Absolute instant nên được truyền bằng ISO-8601 với timezone hoặc offset rõ ràng.
Ví dụ:
{
"startAt": "2026-09-15T14:00:00Z",
"endAt": "2026-09-15T15:30:00Z"
}Frontend có thể hiển thị:
New York → 10:00 AM
Tokyo → 11:00 PMnhưng API vẫn truyền cùng một instant.
Không nên truyền:
{
"startAt": "2026-09-15 10:00"
}vì backend không biết:
10:00 New York?
10:00 London?
10:00 Tokyo?
10:00 server time?Nếu input thực sự là local datetime thì contract nên explicit:
{
"date": "2026-09-15",
"time": "10:00",
"timezone": "America/New_York"
}Backend sau đó mới resolve thành absolute instant.
Local date không phải timestamp
Giả sử user chọn trên calendar:
September 15Dữ liệu này chưa phải instant.
Nó chỉ là:
2026-09-15Nếu developer lập tức chuyển thành:
2026-09-15T00:00:00Zthì semantic đã thay đổi.
Local date trả lời:
Ngày nào trên business calendar?
Timestamp trả lời:
Thời điểm nào trên global timeline?
Vì vậy API hoàn toàn có thể giữ:
{
"date": "2026-09-15"
}và chỉ resolve timezone khi thực sự cần một instant.
Ví dụ:
2026-09-15 + Europe/London + 09:00 -> absolute instant -> UTC4. Recurring schedule và DST
Recurring schedule là nơi rất dễ xảy ra lỗi nếu developer convert local time sang UTC quá sớm.
Giả sử một distributed company có daily standup:
Every Monday. 09:00. Europe/LondonKhông nên convert một lần:
09:00 London → 09:00 UTCrồi assume giá trị UTC đó luôn đúng.
DST có thể khiến UTC representation thay đổi.
Recurring rule nên giữ nguyên semantic:
day_of_week = MONDAY
time = 09:00
timezone = Europe/LondonSau đó mỗi occurrence cụ thể mới được resolve sang UTC.
Mental model:
Recurring rule
↓
Local schedule
↓
Timezone rules
↓
Concrete occurrence
↓
UTC instantPattern này áp dụng cho:
Meetings
Office hours
Scheduled reports
Maintenance windows
Notification campaigns
Remote standups
Market opening times
Vì sao không nên hard-code offset
Giả sử:
09:00 America/New_Yorkở một thời điểm:
= 14:00 UTCnhưng ở thời điểm khác:
= 13:00 UTCNếu developer hard-code:
09:00 New York = 14:00 UTCthì hệ thống sẽ lệch một giờ khi DST thay đổi.
Application không nên tự maintain DST rules.
Thay vào đó, nhiệm vụ chính của application là cung cấp đúng:
local datetime + IANA timezonesau đó để timezone database và datetime library xử lý rule tương ứng.
5. Scheduling trong hệ thống global
Sau khi một occurrence đã được resolve thành instant, phần scheduling trở nên đơn giản hơn nhiều.
Notification nên schedule bằng absolute time
Giả sử event bắt đầu:
14:00 UTCvà cần gửi notification trước 30 phút.
Scheduler chỉ cần tính:
13:30 UTCLúc này scheduler không cần biết participant đang ở Tokyo hay New York.
Core logic:
Event instant
↓
minus 30 minutes
↓
Notification instantKhi render message mới convert sang timezone phù hợp.
Participant ở Tokyo có thể nhìn thấy:
Event starts at 11:00 PMParticipant ở New York có thể nhìn thấy:
Event starts at 10:00 AMScheduling và presentation được tách riêng.
Remote work cũng là một timezone domain
Timezone không chỉ là vấn đề của SaaS có global customers.
Remote team cũng gặp cùng một vấn đề.
Ví dụ:
Product Owner → San Francisco
Tech Lead → London
Developer → Vietnam
QA → IndiaNếu Product Owner nói:
Deploy Monday at 9 AM.
thì thông tin vẫn chưa đủ.
Cần:
Monday
09:00
America/Los_AngelesKhi schedule được xác nhận, system resolve thành UTC.
Sau đó mỗi member có thể nhìn:
San Francisco → 09:00
London → 17:00
Vietnam → 00:00 next day
India → 22:30Một event.
Một instant.
Nhiều local representations.
6. Reporting phải bắt đầu từ Business Timezone
Reporting là nơi timezone bugs rất dễ tồn tại lâu vì dữ liệu nhìn vẫn có vẻ “gần đúng”.
Giả sử cần thống kê:
Daily active users của Tokyo ngày July 20.
Không nên query trực tiếp:
2026-07-20T00:00:00Z → 2026-07-21T00:00:00ZMà phải resolve:
2026-07-20 00:00 Asia/Tokyo → UTC2026-07-21 00:00 Asia/Tokyo → UTCSau đó query:
WHERE created_at >= :utcStart
AND created_at < :utcEndPattern này áp dụng cho:
Daily revenue
Daily active users
Monthly report
Attendance
Conversion
Transactions
KPI
Nếu business question chứa:
day
week
month
quarterthì gần như luôn nên hỏi:
Theo timezone nào?
Dùng half-open interval cho date range
Thay vì model:
00:00:00 → 23:59:59nên dùng:
[start, nextStart)Ví dụ:
WHERE created_at >= :startOfDayUtc
AND created_at < :startOfNextDayUtcCách này tránh phải quan tâm:
milliseconds
microseconds
database precisionĐây là một convention đơn giản nhưng rất hữu ích khi xử lý reporting và date range.
7. Coding conventions để tránh timezone bugs
Timezone logic rất dễ trở nên khó kiểm soát khi codebase xuất hiện khắp nơi:
new Date(...)
dayjs(...)
dayjs.utc(...)
.tz(...)
moment(...)
moment.utc(...)và mỗi developer tự chọn cách convert.
Thay vì vậy nên centralize timezone logic thành shared abstraction.
Ví dụ:
toUtcInstant(...)
toZonedTime(...)
resolveLocalDateTime(...)
getBusinessDayUtcRange(...)
getBusinessLocalDate(...)Một API nội bộ có thể trông như:
resolveLocalDateTime({
date: '2026-09-15',
time: '10:00',
timezone: 'America/New_York',
});và trả:
2026-09-15T14:00:00ZLợi ích lớn nhất không phải chỉ là giảm vài dòng code.
Điểm quan trọng hơn là toàn team sử dụng cùng một convention để xử lý timezone.
Naming nên giữ rõ semantic
Những variable như:
date
time
start
end
nowrất dễ gây nhầm.
Tên rõ semantic hơn:
eventStartUtc
businessTimezone
localBusinessDate
displayTime
utcRangeStart
utcRangeEndKhi nhìn:
eventStartUtcdeveloper biết ngay đây là một instant.
Khi nhìn:
localBusinessDatedeveloper biết đây không phải UTC timestamp.
Timezone bugs thường xuất phát từ việc semantic bị mất trong quá trình dữ liệu đi qua nhiều layer.
Naming tốt giúp giữ lại semantic đó.
8. Mental model và conventions cho team
Có thể tóm tắt architecture timezone bằng hai layer chính:
Business / Local Time
↓
Resolve using timezone
↓
Absolute Instant
↓
UTC Core System
↓
Database / Queue / Scheduler / API
↓
Convert for presentation
↓
Viewer / Business Timezone
Core system có một timeline.
Presentation có nhiều timezone views.
Trước khi xử lý bất kỳ field thời gian nào, có hai câu hỏi nên được đặt ra.
Câu 1: Đây là instant hay local time?
Nếu là:
payment happened
event started
message sent
record createdthì đây là absolute instant.
Thông thường nên normalize thành UTC.
Nếu là:
office opens at 9 AM
every Monday at 10 AM
September 15 on the calendarthì đây là local business time.
Không nên convert thành UTC quá sớm.
Câu 2: Timezone thuộc về ai?
Timezone có thể thuộc về:
Office
Organization
User
Market
Event
ProjectTimezone nên thuộc về entity mang business meaning.
Không nên để:
server timezone
browser timezone
developer timezonevô tình quyết định business logic.
Bộ conventions đề xuất cho team
Một project có thể bắt đầu với các rule sau:
Absolute datetime → canonical UTC.
Absolute database timestamps →
TIMESTAMPTZ.Absolute datetime trong API → ISO-8601 có UTC/offset rõ ràng.
Business entity có timezone → lưu IANA timezone.
Recurring schedule → lưu local wall-clock time + timezone.
Calendar date → giữ
YYYY-MM-DDnếu semantic chỉ là ngày.Duration → không có timezone.
Server timezone → UTC.
Browser timezone → presentation context, không mặc định là business context.
Business date ranges → resolve timezone trước, query UTC sau.
DST → giao cho timezone database/library xử lý.
Timezone conversion → centralize thay vì rải rác trong codebase.
Kết luận
Một timezone architecture tốt không đơn giản là:
Convert tất cả thành UTC.
Đúng hơn phải là:
Hiểu dữ liệu thời gian đang mang semantic gì, timezone thuộc về domain object nào, và tại thời điểm nào dữ liệu đó nên được resolve thành absolute instant.
Mental model có thể rút gọn thành:
Local business time + IANA timezone
↓
Absolute instant
↓
UTC
↓
Storage / API / Queue / Scheduler
↓
Convert for presentationTrong đó:
Business timezone
→ định nghĩa ý nghĩa địa phương của thời gian
UTC
→ tạo một timeline thống nhất cho core system
Display timezone
→ giúp người dùng nhìn cùng một instant theo đúng contextKhi ba layer này được tách rõ, việc customer ở Tokyo, operations ở London, developer ở Vietnam và infrastructure chạy tại Singapore không còn là một case đặc biệt.
Đó chỉ là nhiều góc nhìn khác nhau của cùng một timeline.
Và đó cũng là cách nên nghĩ về timezone khi thiết kế một hệ thống global.