Desarrollo12 min de lectura

Cómo migramos nuestra API de facturación de español a inglés

El proceso real detrás de migrar toda la especificación OpenAPI de BeeL.es a inglés: estrategia de branching, validación incremental y por qué la arquitectura hexagonal nos permitió hacerlo sin tocar el dominio.

11 Febrero 2026

#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í:

Estructura OpenAPI
openapi/
schemas/domain/Modelos de datos
paths/public/ + private/Endpoints
examples/Requests y responses
openapi.yamlArchivo principal

Esto significaba migrar:

  • Nombres de esquemas: CrearFacturaRequestCreateInvoiceRequest
  • Propiedades: direccion_calleaddress_street
  • Valores de enums: FISICA/JURIDICAINDIVIDUAL/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

Arquitectura Hexagonal — Impacto de la migración
InfraestructuraMIGRADA A INGLÉS
REST Controllers
CreateInvoice
DTOs (generados)
CreateInvoiceRequest
Mappers
DTO ↔ Entidad de dominio
OpenAPI Spec
~20.000 líneas
Puertos (Interfaces) — adaptan infra al dominio
AplicaciónSIN CAMBIOS
Use Cases
CrearFacturaUseCase
Application Services
Orquestación de dominio
DominioSIN CAMBIOS
Entidades
Factura, Cliente
Value Objects
NIF, Email
Domain Services
Validaciones

#El impacto real

Todos los cambios se concentraron en cuatro puntos:

  1. Especificación OpenAPI — el contrato público
  2. Controllers — la entrada HTTP
  3. DTOs — generados automáticamente desde la spec
  4. 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í:

FASE 1
Glosario
ES → EN
FASE 2
Schemas
OpenAPI YAML
FASE 3
Lint
Redocly CLI
FASE 4
Code Gen
Maven Plugin
FASE 5
Backend
Controllers, Mappers
FASE 6
Tests
Unit + Integration
FASE 7
DB
Flyway Migration

#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 Flyway

Cada 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 bisect y 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ñolInglésContexto
CrearFacturaRequestCreateInvoiceRequestSchema
direccion_calleaddress_streetProperty
FISICAINDIVIDUALEnum value
JURIDICALEGAL_ENTITYEnum value
numero_facturainvoice_numberProperty
tipo_entidadentity_typeProperty

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:

REQUEST (ENGLISH)
POST /invoices
{ “invoice_number”: “2026-001”, “entity_type”: “INDIVIDUAL” }
CONTROLLER + DTO (ENGLISH)
CreateInvoiceRequest → request.getInvoiceNumber()
MAPPER — Frontera de traducción
dto.getInvoiceNumber() → factura.setNumeroFactura()
DOMINIO (ESPAÑOL — SIN CAMBIOS)
Factura.builder().numeroFactura(“2026-001”).tipoEntidad(FISICA)
BASE DE DATOS (ESPAÑOL)
INSERT INTO facturas (numero_factura, tipo_entidad)

#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:

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.

¿Te ha resultado útil? Compártelo con otros autónomos

Compartir:

¿Listo para simplificar tu facturación?

Únete a BeeL.es y cumple con Verifactu sin complicaciones

7 días gratis — 100% compatible con Verifactu