#Por qué decidimos migrar
Vamos al grano: teníamos toda nuestra API pública en español. Schemas, propiedades, enums, descripciones, ejemplos de requests. Todo. Y cuando empezamos a pensar en serio en la API como producto — no solo como herramienta interna — nos dimos cuenta de que eso era un problema.
No porque el español sea peor que el inglés para documentar software. Sino porque la realidad del ecosistema de desarrollo es que la mayoría de desarrolladores esperan leer una API en inglés. Da igual si tu producto es español, alemán o japonés. Si tu API expone CrearFacturaRequest con una propiedad direccion_calle, estás añadiendo fricción innecesaria a cualquier integrador que no hable español.
Y en España hay más de los que imaginas: miles de emprendedores extranjeros que montan negocios aquí cada año, necesitan facturar conforme a la normativa española, y las opciones de software con APIs accesibles en inglés son prácticamente inexistentes.
ℹ️El problema real
Un desarrollador alemán que monta una startup en Barcelona necesita facturar. Busca APIs de facturación española. Encuentra documentación solo en español. Pasa de largo. No es un problema técnico, es un problema de accesibilidad.
La decisión fue clara. Lo que no estaba tan claro era cómo ejecutarla sin romper medio sistema — y siendo dos personas.
#El alcance real del cambio
Antes de tocar nada, hicimos un inventario. Nuestra especificación OpenAPI tenía alrededor de 20.000 líneas distribuidas así:
Esto significaba migrar:
- Nombres de esquemas:
CrearFacturaRequest→CreateInvoiceRequest - Propiedades:
direccion_calle→address_street - Valores de enums:
FISICA/JURIDICA→INDIVIDUAL/LEGAL_ENTITY - Descripciones y summaries de cada endpoint
- Todos los examples de requests/responses
- Mensajes de error y validaciones
- Código Java downstream: Controllers, Mappers, DTOs
- Tests de integración y unitarios
- Migraciones de base de datos (variables de plantillas de email)
Un cambio de este calibre puede romper la integridad de la spec, generar inconsistencias en el código generado, o introducir bugs sutiles que no saltan hasta producción. Pero teníamos algo a nuestro favor: la arquitectura del sistema.
#Por qué la arquitectura hexagonal hizo esto viable
BeeL.es está construido con arquitectura hexagonal (Ports & Adapters) combinada con DDD. No vamos a explicar el patrón desde cero — si no lo conoces, al final del artículo dejamos recursos — pero sí queremos contar por qué fue determinante en esta migración.
La idea fundamental es sencilla: todo lo que es API (controllers, DTOs, specs) vive en la capa de infraestructura. La lógica de negocio — los casos de uso que crean facturas, validan datos fiscales, calculan impuestos — vive en el dominio, completamente aislada. Entre ambas capas hay una frontera clara: los mappers.
Esto significaba que podíamos reescribir completamente el contrato público de la API sin tocar ni una línea del dominio. Y eso es exactamente lo que hicimos.
#Cómo está organizado nuestro sistema
#El impacto real
Todos los cambios se concentraron en cuatro puntos:
- Especificación OpenAPI — el contrato público
- Controllers — la entrada HTTP
- DTOs — generados automáticamente desde la spec
- Mappers — la frontera de traducción entre infra y dominio
El dominio no se tocó. Ni una línea. Los casos de uso, las entidades, las validaciones de negocio, la persistencia — todo intacto. Eso es la promesa de la arquitectura hexagonal y en esta migración se cumplió al pie de la letra.
Además, al usar openapi-generator-maven-plugin para generar los DTOs, el rename de esquemas en el YAML se propagó automáticamente al código Java. Solo tuvimos que actualizar las referencias manuales en Controllers y Mappers.
#La estrategia: branching por fase, merge incremental
Lo primero que descartamos fue hacer todo en una sola branch gigante. Un diff de 20.000 líneas es inrevisable. Si algo se rompe, no sabes dónde. Y si otro desarrollador necesita mergear trabajo en paralelo, los conflictos serían imposibles de resolver.
En su lugar, diseñamos una estrategia de branching por fase. Cada fase de la migración vivió en su propia feature branch, con su propio PR y su propia validación. El flujo fue así:
#Estrategia de branching
Trabajamos con una branch por fase, todas partiendo de develop. La convención fue:
feature/api-i18n-glossary ← Fase 1: glosario de traducciones
feature/api-i18n-schemas ← Fase 2: schemas OpenAPI
feature/api-i18n-paths ← Fase 3: paths, descriptions, examples
feature/api-i18n-codegen ← Fase 4: regeneración de DTOs
feature/api-i18n-controllers ← Fase 5: controllers + mappers
feature/api-i18n-tests ← Fase 6: tests
feature/api-i18n-db ← Fase 7: migraciones FlywayCada branch se mergeaba a develop solo cuando el PR pasaba code review y CI estaba verde. La branch siguiente siempre partía del HEAD de develop después del merge anterior. Esto nos daba varias cosas:
- Diffs revisables: cada PR tenía un scope acotado. El PR de schemas era grande (~4.000 líneas), pero era solo YAML — fácil de revisar.
- Bisect limpio: si algo se rompía más adelante, podíamos hacer
git bisecty localizar exactamente en qué fase se introdujo el problema. - Sin bloquearse mutuamente: mientras uno de los dos tenía una fase en review, el otro podía seguir avanzando en
develop. Los conflictos solo aparecían en los puntos de merge, y al ser incrementales, eran manejables.
⚠️Lo que no hicimos
No usamos una long-lived branch tipo feature/api-migration con todo junto. Tampoco hicimos rebase agresivo entre fases. Merge commits explícitos, para que el historial contara la historia real de cómo fue el proceso.
#Fase 1: El glosario como fuente de verdad
Antes de tocar código, construimos un glosario de traducciones — un documento con todas las correspondencias entre términos en español e inglés. Parece básico, pero fue lo que evitó inconsistencias como tener customer en un endpoint y client en otro, o invoice_number contra invoice_id para el mismo concepto.
| Español | Inglés | Contexto |
|---|---|---|
| CrearFacturaRequest | CreateInvoiceRequest | Schema |
| direccion_calle | address_street | Property |
| FISICA | INDIVIDUAL | Enum value |
| JURIDICA | LEGAL_ENTITY | Enum value |
| numero_factura | invoice_number | Property |
| tipo_entidad | entity_type | Property |
Cada traducción la discutimos entre los dos. ¿entity_type o entity_kind? ¿INDIVIDUAL o NATURAL_PERSON? Estas decisiones se toman una vez, se documentan, y después se siguen sin desviación. El glosario vivió en el repo como un markdown dentro de /docs y fue referenciado en todos los PRs.
#Fase 2-3: Migración de la spec OpenAPI
Con el glosario cerrado, la migración de la spec fue metódica. Primero los schemas, después los paths y examples. Un ejemplo de cómo quedaba un schema:
# Antes
CrearFacturaRequest:
type: object
properties:
numero_factura:
type: string
direccion_calle:
type: string
tipo_entidad:
enum: [FISICA, JURIDICA]# Después
CreateInvoiceRequest:
type: object
properties:
invoice_number:
type: string
address_street:
type: string
entity_type:
enum: [INDIVIDUAL, LEGAL_ENTITY]Después de cada grupo de cambios, lanzábamos redocly lint openapi.yaml. No al final — después de cada cambio significativo. Una referencia rota en un schema se detecta en segundos si validas al momento. Si la dejas pasar y migras 50 schemas más encima, el error se multiplica y rastrearlo es un infierno.
Integramos Redocly en CI para que ningún PR pudiera mergearse con una spec inválida:
# En el pipeline de CI
- name: Validate OpenAPI
run: npx @redocly/cli lint openapi.yaml --format=stylish#Fase 4: Regeneración de DTOs
Una vez la spec era válida en inglés, un mvn clean generate-sources regeneró todos los DTOs automáticamente. El plugin openapi-generator-maven-plugin nos dio:
- Nuevos DTOs con nombres en inglés
- Interfaces de API actualizadas
- Modelos de validación
Aquí es donde se nota el valor del code generation. Sin él, habríamos tenido que renombrar manualmente cientos de clases Java. Con él, fue un comando.
#Fase 5: Controllers y Mappers — la frontera de traducción
Los controllers pasaron a usar los nuevos DTOs:
// Antes
@PostMapping("/facturas")
public FacturaResponse crearFactura(
@RequestBody CrearFacturaRequest request) { ... }
// Después
@PostMapping("/invoices")
public InvoiceResponse createInvoice(
@RequestBody CreateInvoiceRequest request) { ... }Pero el cambio más interesante está en los mappers. Es donde ocurre la traducción de idioma en el flujo de datos — el DTO habla inglés, la entidad de dominio sigue hablando español:
// El mapper actúa como frontera de traducción
Factura toEntity(CreateInvoiceRequest dto) {
return Factura.builder()
.numeroFactura(dto.getInvoiceNumber())
.direccionCalle(dto.getAddressStreet())
.build();
}Esto es un detalle que merece atención: el dominio sigue en español. Factura, numeroFactura, tipoEntidad — todo intacto. El mapper traduce entre el contrato público (inglés) y el dominio interno (español). Esa frontera está claramente definida gracias a la arquitectura hexagonal, y es lo que hizo posible esta migración.
Este es el flujo completo de una petición, con la frontera de traducción marcada:
#Fase 6: Tests
Los tests de integración y unitarios se actualizaron para usar los nuevos DTOs:
@Test
void shouldCreateInvoiceCorrectly() {
CreateInvoiceRequest request = new CreateInvoiceRequest()
.invoiceNumber("2026-001")
.addressStreet("Calle Mayor 1");
// ...
}Teníamos cobertura por encima del 80%, así que cada fase se validó ejecutando la suite completa de tests. Ningún PR se mergeaba sin verde en CI. Esto nos dio la confianza de hacer cambios agresivos — si rompíamos algo, lo sabíamos en minutos, no en producción.
#Fase 7: La base de datos — lo que casi se nos escapa
Este es el punto que más nos pillaba desprevenidos. Pensábamos que la migración era “solo código y specs”. Pero teníamos plantillas de email en base de datos con variables como {{numero_factura}} que necesitaban migrar a {{invoice_number}}.
Creamos una migración Flyway para resolverlo:
UPDATE email_templates
SET template_body = REPLACE(
REPLACE(template_body, '{{numero_factura}}', '{{invoice_number}}'),
'{{direccion_calle}}', '{{address_street}}'
);La lección aquí: audita tus datos antes de empezar una migración así. No solo el código. Cualquier dato persistido que referencie la nomenclatura antigua es un punto de fallo.
#El tooling que usamos
Resumiendo las herramientas que hicieron el proceso viable:
- Redocly CLI para lint de la spec OpenAPI en cada commit y en CI
- openapi-generator-maven-plugin para regenerar DTOs automáticamente desde la spec
- IntelliJ refactoring (Shift+F6, Structural Search) para renombrar símbolos Java con seguridad en controllers y mappers
- Flyway para migraciones de base de datos versionadas
- Claude para generar el glosario inicial, revisar traducciones de descripciones técnicas y detectar inconsistencias. Útil para las partes repetitivas, pero todas las decisiones las tomamos nosotros
- Git con branching por fase como ya hemos descrito — PRs incrementales, CI obligatorio, merge a develop
#Lo que aprendimos
Algunos takeaways que nos quedan después de esta migración:
La arquitectura te salva o te hunde. Podríamos haber tenido las mejores herramientas del mundo — si la arquitectura hubiera estado acoplada, esto habría sido semanas de debugging. La inversión en hexagonal se pagó sola en esta migración.
Code generation no es opcional en una API seria. Generar DTOs desde la spec elimina una categoría entera de bugs (desincronización spec-código) y convierte refactorizaciones masivas en un comando de Maven.
Branching granular > una mega-branch. Es tentador meterlo todo en una branch y hacer un gran merge al final. No lo hagas. Los PRs pequeños se revisan mejor, los conflictos se resuelven antes de que crezcan, y si algo se rompe, git bisect te dice exactamente dónde.
Valida pronto, valida siempre. Un error de referencia en un schema es trivial de arreglar en el momento. Descubierto después de migrar 50 schemas encima, es un efecto dominó. Redocly en CI fue no negociable.
Audita tus datos, no solo tu código. Casi nos pilla por sorpresa que la base de datos también tenía nomenclatura en español embebida en plantillas. Antes de empezar cualquier migración de este tipo, haz un grep en tus datos persistidos.
Documenta las decisiones, no solo el resultado. El glosario fue vital. Cuando a mitad de migración alguien preguntaba “¿por qué entity_type y no entity_kind?”, la respuesta estaba documentada. Sin eso, habrías tenido debates repetidos y decisiones inconsistentes.
#Si te enfrentas a algo parecido
1Invierte en arquitectura desde el día 1
Si vas a construir una API que evolucionará con el tiempo, merece la pena empezar con hexagonal. El overhead inicial es real, pero la primera vez que necesitas hacer un cambio transversal como este, se amortiza.
2Genera código desde la spec, no al revés
No escribas DTOs a mano. Usa openapi-generator (Java), oapi-codegen (Go), o lo que corresponda a tu stack. La spec es la fuente de verdad, el código generado es un artefacto.
3Branches pequeñas, merges frecuentes
Una branch por fase. PR con scope acotado. CI verde obligatorio antes de merge. Merge commits explícitos para mantener el historial legible. Nada de long-lived branches con 15.000 líneas de diff.
4Lint en CI, no como afterthought
redocly lint o spectral en cada push. Si la spec no valida, el PR no se mergea. Así de simple.
5Tests como red de seguridad real
No tests decorativos — tests que cubran los flujos críticos. Nuestra cobertura >80% nos dio la confianza para hacer cambios agresivos sabiendo que si rompíamos algo, lo íbamos a saber.
6Si tu API ya tiene usuarios, planifica la transición
Nosotros tuvimos la suerte de que la API pública aún no estaba en producción. Si la tuya sí lo está: deprecation notices, versionado (v1/v2), periodo de transición. No hagas breaking changes sin avisar.
#Conclusión
Migramos ~20.000 líneas de spec OpenAPI, regeneramos los DTOs Java, actualizamos controllers, mappers, tests y datos en base de datos. El dominio — los casos de uso, las entidades, la lógica de negocio — no se tocó. Cero líneas cambiadas en el núcleo.
Eso no fue suerte. Fue el resultado de haber invertido en una arquitectura que separa infraestructura de dominio desde el principio. Y de haber ejecutado la migración con una estrategia de branching que nos permitió avanzar de forma incremental, con validación en cada paso.
Si estás construyendo un producto que va a evolucionar — y todo producto serio lo hace — la arquitectura no es un gasto. Es la inversión que te permite hacer cambios así sin que el sistema se caiga.
#Preguntas frecuentes
#Recursos
Si quieres profundizar en los temas que hemos tocado:
Arquitectura Hexagonal y DDD:
- Alistair Cockburn: Hexagonal Architecture — el artículo original del creador del patrón
- AWS Prescriptive Guidance: Hexagonal Architecture
- Baeldung: Hexagonal Architecture, DDD, and Spring — implementación práctica en Spring Boot
OpenAPI y Code Generation:
Si estás buscando un software de facturación con una API diseñada para desarrolladores, echa un vistazo a nuestra API pública.
