1720 palabras
9 minutos
Fechas en JPA y Spring Boot

Introducción#

El manejo de fechas es uno de esos temas que parece simple al principio, pero que esconde una complejidad importante cuando construimos aplicaciones. En este artículo, vamos a descubrir los principales problemas que pueden surgir al trabajar con fechas y cómo podemos evitarlos con un enfoque adecuado.

El Problema Oculto de las Fechas#

Antes de sumergirnos en el código, entendamos por qué trabajar con fechas y horas puede ser un tema algo complejo y de alto riesgo cuando no se maneja correctamente.

Imaginemos un e-commerce con presencia internacional que lanza una promoción flash de 50% de descuento válida por solo 1 hora. El equipo de marketing ubicado en Lima, Perú programa la promoción para que inicie mañana a las 2:00 PM sin tener en cuenta las diferentes zonas horarias. Al ver esta fecha en la página web, cada usuario asume que será en su zona horaria local. Sin embargo, esto es lo que sucede el día de la promoción:

  • 🇵🇪 Anthony - Lima, Perú: Ingresa a la página web a las 2:00 PM y aprovecha la promoción. ✅
  • 🇨🇱 Lucía - Santiago, Chile: Ingresa a la página web a las 2:00 PM, pero la promoción aún no ha comenzado (comenzará a las 3:00 PM hora Chile). ❌
  • 🇦🇷 Luis - Buenos Aires, Argentina: Ingresa a la página web a las 2:00 PM, pero la promoción aún no ha comenzado (comenzará a las 4:00 PM hora Argentina). ❌

¿Qué pasó exactamente?#

Cuando son las 2:00 PM en Lima 🇵🇪, en ese mismo momento son las:

  • 🇨🇱 3:00 PM en Santiago (1 hora adelante)
  • 🇦🇷 4:00 PM en Buenos Aires (2 horas adelante)

Esto nos lleva a la reflexión de que sin un manejo adecuado de zonas horarias, nuestras aplicaciones pueden fallar silenciosamente, generar bugs difíciles de detectar y afectar la lógica de negocio.

Principales Formas de Trabajar con Fechas#

Con estas tres formas de representar fechas cubriremos la mayoría de casos de uso en aplicaciones Spring Boot. Son las opciones más comunes y recomendadas para empezar.

1. LocalDate: Fechas Sin Hora#

LocalDate es perfecto cuando solo necesitamos representar una fecha, sin información de hora ni zona horaria. Por ejemplo, tenemos una entidad Event que representa un evento y queremos almacenar la fecha de inicio del evento:

@Entity
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private LocalDate eventDate;
// Constructores, getters y setters...
}

Comportamiento#

Se almacena eventDate en la BD como una fecha sin hora ni zona horaria Ejm: 2025-07-02.

Cuándo usarlo#

  • ✅ Fechas de eventos que no dependen de la hora (ejm: nacimientos, aniversarios, vacaciones, etc.)

2. LocalDateTime: Fechas con Hora (Sin Zona Horaria)#

LocalDateTime combina fecha y hora, pero no incluye información de zona horaria. Esto significa que representa una fecha y hora “local” sin contexto temporal absoluto; es decir, no se sabe en qué zona horaria fue generada o a cuál pertenece. Por ejemplo, tenemos una entidad Product que representa un producto y queremos almacenar la fecha y hora de vencimiento:

@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private LocalDateTime expirationDate;
// Constructores, getters y setters...
}

Comportamiento#

Se almacena expirationDate en la BD como una fecha y hora sin zona horaria Ejm: 2025-07-02 09:00:00.

Cuándo usarlo#

  • ✅ Aplicaciones que operan exclusivamente en un país o región
  • ✅ Sistemas internos donde todos los usuarios están en la misma ubicación
IMPORTANT

Hay que tener en cuenta que muchos países como Estados Unidos 🇺🇸, México 🇲🇽, Brazil 🇧🇷, Chile 🇨🇱, etc. tienen más de una zona horaria u horario de verano/invierno. Por lo tanto, LocalDateTime puede generar ambigüedad en dichos casos.

3. Instant: Timestamps en UTC#

Instant representa un momento específico en la línea temporal UTC. Es ideal para almacenar timestamps precisos y es la mejor opción para aplicaciones que necesitan manejar eventos globales. Por ejemplo, tenemos una entidad Meeting que representa una reunión y queremos almacenar la fecha y hora de la reunión:

@Entity
public class Meeting {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Instant scheduledDate;
// Constructores, getters y setters...
}

Comportamiento#

Se almacena scheduledDate en la BD como un timestamp en UTC. Ejm: una fecha y hora peruana (UTC-5) 2025-07-02 10:00:00 se almacena como 2025-07-02 15:00:00.

Cuándo usarlo#

  • ✅ Timestamps de auditoría
  • ✅ Aplicaciones globales donde los usuarios están en diferentes zonas horarias
  • ✅ Sistemas que requieren alta precisión temporal
TIP

UTC se refiere a “Coordinated Universal Time”, es el estándar de tiempo global en el que se basan todas las zonas horarias. Por ejemplo, la zona horaria de Lima, Perú es “America/Lima” y su offset es UTC-5, lo que significa que está 5 horas detrás de UTC. En cambio, Tokio, Japón es “Asia/Tokyo” y su offset es UTC+9, lo que significa que está 9 horas adelante de UTC.

Otras Formas de Trabajar con Fechas#

Estas opciones son un poco más complejas y generalmente se usan en escenarios que requieren un manejo especial de zonas horarias y fechas.

IMPORTANT

JPA no soporta estas clases como tipo de dato para columnas de una entidad, por lo que se pierde la información de zona horaria original y solo se almacenan los timestamps en UTC. Sin embargo, con Hibernate es posible preservar esta información usando anotaciones especiales. Más información

4. OffsetDateTime: Fechas con Offset Fijo Respecto a UTC#

OffsetDateTime combina fecha, hora y un offset fijo respecto a UTC (como “+02:00” o “-05:00”). Es útil cuando necesitamos preservar el contexto temporal exacto de una zona horaria específica. Veamos un ejemplo de cómo implementarlo y usarlo en la lógica de negocio:

@Entity
public class TransactionWithOffset {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double amount;
// Almacenamos el timestamp en UTC
private Instant transactionDate;
// Almacenamos el offset como String
private String timeOffset;
// Método helper para obtener OffsetDateTime completo
public OffsetDateTime getTransactionDateWithOffset() {
return transactionDate.atOffset(ZoneOffset.of(timeOffset));
}
// Método helper para establecer desde OffsetDateTime
public void setTransactionDate(OffsetDateTime offsetDateTime) {
this.transactionDate = offsetDateTime.toInstant();
this.timeOffset = offsetDateTime.getOffset().toString(); // Ejm: "+05:00"
}
}

5. ZonedDateTime: Fecha, Hora y Zona Horaria#

ZonedDateTime es similar a OffsetDateTime, pero incluye la zona horaria completa (como “America/Lima” o “Europe/Madrid”) en lugar de solo un offset fijo. Esto permite que el offset se calcule automáticamente según las reglas de la zona horaria y la fecha específica. Veamos un ejemplo:

@Entity
public class AppointmentWithTimezone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
// Almacenamos el timestamp en UTC
private Instant appointmentTime;
// Almacenamos la zona horaria como String
private String timeZone;
// Método helper para obtener ZonedDateTime completo
public ZonedDateTime getAppointmentWithTimezone() {
return appointmentTime.atZone(ZoneId.of(timeZone));
}
// Método helper para establecer desde ZonedDateTime
public void setAppointmentTime(ZonedDateTime zonedDateTime) {
this.appointmentTime = zonedDateTime.toInstant();
this.timeZone = zonedDateTime.getZone().getId(); // Ejm: "America/Lima"
}
// Método para convertir a otra zona horaria
public ZonedDateTime getAppointmentInTimezone(String targetTimezone) {
return appointmentTime.atZone(ZoneId.of(targetTimezone));
}
}

Forma legacy de trabajar con fechas#

Las anteriores clases que vimos son enfoques modernos disponibles desde Java 8 (en el paquete java.time). Sin embargo, existe una clase anterior que aún se encuentra en muchos proyectos pero que no es recomendable usar: Date.

6. Date (No Recomendado)#

Date es una clase legacy de Java que, aunque funcional, tiene serias limitaciones técnicas que pueden causar problemas en aplicaciones modernas. Su uso se desaconseja por las siguientes razones que veremos a continuación.

Primero, veamos cómo se vería en código:

@Entity
public class LegacyEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Almacenamos la fecha. También podemos usar @Temporal(TemporalType.TIMESTAMP) o @Temporal(TemporalType.TIME)
@Temporal(TemporalType.DATE)
private Date eventDate;
// Constructores, getters y setters...
}

Comportamiento#

Dependiendo de la anotación que usemos, se almacenará eventDate en la BD de diferentes formas:

  • TemporalType.DATE: Solo fecha (año, mes, día). Ejm: 2025-07-02
  • TemporalType.TIME: Solo hora (hora, minutos, segundos). Ejm: 10:00:00
  • TemporalType.TIMESTAMP: Fecha y hora completa. Ejm: 2025-07-02 10:00:00

Importante: El valor se guarda usando la zona horaria del servidor/JVM, pero la BD no almacena información de zona horaria, lo que puede causar problemas al cambiar servidores o zonas horarias.

¿Por qué no es recomendable?#

  • API confusa y obsoleta: Muchos métodos están deprecados y devuelven resultados no intuitivos (Ejm: meses desde 0, años desde 1900)
  • Es mutable: Permite modificar su estado, lo que puede causar errores en aplicaciones multihilo o cuando se comparte el objeto
  • No tiene zona horaria: No maneja zonas horarias de manera clara, lo que puede provocar errores al trabajar con usuarios en diferentes regiones o cuando se cambia la zona horaria del servidor
  • Precision limitada: Solo permite manejar milisegundos, no nanosegundos
  • Difícil de usar: Operaciones básicas como sumar o restar días, o formatear la fecha requieren código complejo u otras clases auxiliares

Cuándo usarlo#

  • ⚠️ Solo en aplicaciones legacy que requieren compatibilidad con código existente
  • ⚠️ Integración con bibliotecas antiguas que solo aceptan Date
TIP

Si estamos trabajando con una aplicación que usa esta clase, hay que considerar migrar gradualmente hacia las clases modernas según las necesidades, esto mejorará la mantenibilidad y reducirá bugs relacionados con fechas.

Recomendación: Trabajar con UTC#

Cuanto más lo pensamos, más complejo se vuelve el tema de las fechas; por lo tanto, para la mayoría de aplicaciones, es más simple y efectivo trabajar con Instant (UTC) en el backend y manejar las conversiones de zona horaria en la capa de presentación o en las aplicaciones cliente. Esto ofrece múltiples ventajas:

  • Simplicidad en la BD: Una sola columna timestamp, sin campos adicionales
  • Consistencia global: Todos los timestamps están en la misma base temporal
  • Facilita ordenamientos: Los timestamps son directamente comparables
  • Sin bugs de zona horaria: Elimina errores por cambios de horario de verano/invierno
  • Escalabilidad: Los servidores pueden estar en diferentes zonas horarias sin problemas
  • Fechas adaptadas: Las aplicaciones cliente (frontend) se adaptan automáticamente a la zona horaria del usuario, esto quiere decir que si el usuario usa la aplicación en Lima, Sao Paulo o Tokyo la fecha y hora se mostrarán correctamente en su zona horaria.

Sobre la serialización de fechas en JSON#

Por defecto, Spring Boot con Jackson (la biblioteca por defecto para serializar y deserializar JSON) serializa las fechas en formato ISO 8601. Por ejemplo:

  • LocalDate: "2025-07-02"
  • LocalDateTime: "2025-07-02T15:00:00"
  • Instant: "2025-07-02T15:00:00Z"
  • OffsetDateTime: "2025-07-02T15:00:00+05:00"
  • ZonedDateTime: "2025-07-02T15:00:00+05:00[America/Lima]"

Se recomienda mantener este formato ya que es el estándar para representar fechas y horas en APIs y en internet en general. A continuación, un diagrama que ilustra las fechas en formato ISO 8601 y sus diferentes representaciones:

Fechas en formato ISO 8601

Conclusión#

El manejo correcto de fechas en Spring Boot y JPA no tiene por qué ser complejo si seguimos principios claros y consistentes. La clave está en adoptar UTC como el estándar universal y mantener las conversiones de zona horaria en las aplicaciones cliente. Además, implementar estas prácticas desde el inicio nos ahorrará horas de debugging y nos dará la confianza de que nuestra aplicación manejará fechas correctamente sin importar dónde estén nuestros usuarios.

Fechas en JPA y Spring Boot
https://blog.miikuru002.dev/posts/dates-in-jpa-and-spring-boot/
Autor
J. Ortega
Publicado el
2025-07-02
Licencia
CC BY-NC-SA 4.0