Retrato en blanco y negro de Daniel Ortega con los brazos cruzados

Daniel Ortega

Ingeniero full-stack senior. Del modelo de dominio al último píxel.

Más de 8 años construyendo producto con PHP, Symfony, TypeScript y React. Ahora en Sesame HR; antes en The Knot Worldwide, Planeta Huerto y Square1. En paralelo, mis propias apps.

Ahora en Sesame HRValencia, España

GET / —
DNS—
Conexión—
Servidor—
Descarga—
DOM listo—
Primer pintado—
Mayor pintado—
RedServidorNavegador
Medido ahora mismo en tu navegador. No es una captura.

Trabajo

Tres productos propios. De la idea a la app, yo solo.

Un hilo común: software honesto con sus datos. Si algo es una estimación, lo dice. Si falta un dato, no se lo inventa.

ToneMate

En beta

Selecciona texto. Pulsa un atajo.

Asistente de escritura nativo para macOS. Reescribe, traduce, corrige o responde donde estés escribiendo, con un modelo en tu Mac o con tus propias API keys. También dicta voz a texto.

Decisiones de ingeniería

  1. Tu texto no pasa por mis servidores.

    Modelos locales con Ollama o MLX, o tus claves guardadas en el Llavero de macOS. La app solo me contacta para comprobar la licencia.

  2. Un modelo se prueba antes de confiar en él.

    «Probar en este Mac» ejecuta 12 comprobaciones reales de idioma, erratas y velocidad sobre el modelo local.

  3. Sin robarte el portapapeles.

    Captura la selección con el portapapeles, inserta el resultado y luego deja lo que tenías copiado. Funciona donde puedas seleccionar y copiar texto.

Solicitar acceso

macOS · Ollama · MLX · Whisper · Parakeet

Ajustes de modelos de ToneMate, con modelos locales y de proveedores en la nube
Panel de ToneMate reescribiendo un mensaje informal en tono profesional con un modelo local

GoForIt

Beta muy pronto

Cuánto margen tienes hoy, y por qué.

Tu sistema personal de rendimiento: entrenamiento, nutrición, sueño y recuperación en un solo lugar. Para iPhone y Apple Watch, conectado con Apple Health, Google Health, Strava y Garmin.

Decisiones de ingeniería

  1. Te compara contigo, no con una media.

    La preparación de 0 a 100 combina sueño, variabilidad del pulso, pulso en reposo y carga reciente, siempre frente a tu propia línea base.

  2. Dice lo que no sabe.

    Cada número lleva confianza y rango probable. Si el de hoy aún no existe, lo dice: nunca enseña el de ayer como si fuera de hoy.

  3. Sin muestra no hay conclusión.

    Solo habla de lo que te sienta bien con al menos 5 días con y 5 sin en los últimos 90, y enseña cuántos hay detrás. Es asociación, y lo dice.

Visitar la web

iOS · watchOS · Apple Health · Strava · Garmin

Pantalla Hoy de GoForIt: preparación estimada de 74 sobre 100, con confianza media y rango de 70 a 78
Pantalla de entrenamiento de GoForIt
Diario de GoForIt, con todo lo registrado ordenado por día

Pedalytics

Beta muy pronto

100 km no siempre son 100 km.

Desgaste y mantenimiento de tu bicicleta. Conecta tus salidas, asigna cada pieza a su bici y entiende cuánto desgasta de verdad cada ruta: separa la distancia recorrida de la carga mecánica.

Decisiones de ingeniería

  1. El historial no se reescribe.

    Cada salida guarda su propia fotografía del montaje. Cambias la cadena el martes y la salida del domingo sigue ligada a la cadena vieja.

  2. Sin cifras inventadas.

    Cada intervalo cita su origen, del fabricante o de una medición independiente. Donde no hay dato fiable propone una inspección, no un kilometraje.

  3. Precedencia explícita.

    Si el proveedor sabe qué bici fue, se usa. Si no, se infiere y queda marcado. Y lo que corriges tú manda siempre.

Visitar la web

Strava · Archivos FIT · +3.500 componentes

Pantalla de desgaste de Pedalytics: cadena, cassette y pastillas con su porcentaje, y el cuadro con inspección periódica
Detalle de una salida en Pedalytics: 84 km de distancia, 112 km de carga y el montaje que llevaba la bici
Garaje de Pedalytics con dos bicicletas y sus alertas de desgaste

Trayectoria

Mi carrera, leída como una traza.

Cada span es una etapa. Ábrelo para ver sus atributos.

carrera8 años 8 meses
Square1Software Engineer3 años 8 meses
Periodo
mar 2018 – nov 2021
Ubicación
Dublín (remoto)
Stack
PHP, Laravel, MySQL, Webpack, AWS
Web
square1.io

Qué hice

  • Participación directa en discovery con managers y clientes.
  • Liderazgo técnico puntual en proyectos y mantenimiento a largo plazo.
  • Integraciones de pago (Stripe, PayPal) con impacto medible en conversión.

Impacto

  • Entregas más predecibles en proyectos con múltiples stakeholders.
  • Mejora de la conversión en flujos de pago en varios clientes.
  • Base técnica más estable para evoluciones posteriores y mantenimiento.
Planeta HuertoSoftware Engineer1 año
Periodo
nov 2021 – nov 2022
Ubicación
Alicante (remoto)
Stack
PHP, Symfony, MySQL, Docker, Google Cloud
Web
planetahuerto.es

Qué hice

  • Funcionalidades de ecommerce con fuerte orientación a negocio y operaciones.
  • Prácticas de arquitectura: SOLID, DDD, CQRS y diseño hexagonal.
  • Documentación técnica y funcional para acelerar onboarding y mantenimiento.

Impacto

  • Mejor alineación entre necesidades de negocio y ejecución técnica en el roadmap.
  • Mayor mantenibilidad en módulos críticos gracias a decisiones de arquitectura más claras.
  • Onboarding técnico más rápido por documentación estructurada y útil.
The Knot WorldwideSenior Software Engineer3 años 1 mes
Periodo
nov 2022 – dic 2025
Ubicación
Barcelona (remoto)
Stack
PHP, Symfony, TypeScript, React, GraphQL, AWS
Web
theknotww.com

Qué hice

  • Entrega continua sobre un stack moderno con TypeScript, React y GraphQL.
  • Colaboración diaria con Producto, Diseño y QA para elevar la calidad de cada release.
  • Mejora de procesos de desarrollo para reducir fricción y tiempo de entrega.

Impacto

  • Mayor estabilidad en releases y menos hotfixes de última hora.
  • Más velocidad de entrega en features cross-team gracias a una mejor coordinación técnica.
  • Menos fricción en el desarrollo diario con convenciones y herramientas compartidas.
Productos propiosDesarrollo independienteEn curso
Periodo
En curso

Diseño, construyo y lanzo mis propias apps, de la idea al producto, con la misma ingeniería que aplico en mi día a día.

Sesame HRSenior Software Engineer7 meses
Periodo
abr 2026 – En curso
Ubicación
Valencia
Web
sesamehr.es

Trabajo en Sesame HR, en Valencia: una plataforma de recursos humanos que reúne fichajes, vacaciones, nóminas, talento y comunicación.

Ver la experiencia completa

Cómo construyo

Una feature, todas las capas.

Full-stack no es saber un poco de todo. Es seguir un cambio desde el botón hasta la base de datos sin perder el hilo. Aquí, cancelar un pedido capa a capa, con las dependencias apuntando hacia dentro.

Capa del sistema

Las dependencias siempre apuntan hacia el dominio.

La interfaz responde al instante y el error se traduce a algo que el usuario entiende. No conoce ninguna regla de negocio.

CancelOrderButton.tsx

export function CancelOrderButton({ orderId }: { orderId: string }) {
  const [cancelOrder, { loading, error }] = useMutation(CANCEL_ORDER, {
    variables: { id: orderId },
    optimisticResponse: {
      cancelOrder: { __typename: "Order", id: orderId, status: "CANCELLED" },
    },
  });

  if (error) return <Alert>{messageFor(error)}</Alert>;

  return (
    <Button onClick={() => cancelOrder()} disabled={loading}>
      Cancelar pedido
    </Button>
  );
}

El contrato es la frontera entre equipos. Los errores esperables forman parte de él, no son una sorpresa en producción.

order.graphql

type Mutation {
  "Falla con ORDER_ALREADY_SHIPPED si el pedido ya ha salido."
  cancelOrder(id: ID!): Order!
}

type Order {
  id: ID!
  status: OrderStatus!
  cancelledAt: DateTime
}

enum OrderStatus {
  PAID
  SHIPPED
  CANCELLED
}

Orquesta y no decide. Depende de interfaces, así que se lee como la descripción del caso de uso.

CancelOrderHandler.php

#[AsMessageHandler]
final readonly class CancelOrderHandler
{
    public function __construct(
        private OrderRepository $orders,
        private ClockInterface $clock,
        private EventBus $events,
    ) {}

    public function __invoke(CancelOrder $command): void
    {
        $order = $this->orders->get(new OrderId($command->orderId));

        $order->cancel($this->clock->now());

        $this->orders->save($order);
        $this->events->publish(...$order->releaseEvents());
    }
}

Aquí viven las reglas. Sin framework y sin base de datos: solo el negocio, escrito en su propio idioma.

Order.php

final class Order
{
    use RecordsEvents;

    public function cancel(DateTimeImmutable $now): void
    {
        if ($this->status === OrderStatus::Shipped) {
            throw OrderAlreadyShipped::withId($this->id);
        }

        if ($this->status === OrderStatus::Cancelled) {
            return; // Idempotente: cancelar dos veces no es un error.
        }

        $this->status = OrderStatus::Cancelled;
        $this->cancelledAt = $now;
        $this->record(new OrderCancelled($this->id, $now));
    }
}

El dominio define lo que necesita y la infraestructura lo cumple. Cambiar de base de datos es cambiar un adaptador.

OrderRepository.php

// Dominio: el puerto no sabe nada de Doctrine.
interface OrderRepository
{
    public function get(OrderId $id): Order;
    public function save(Order $order): void;
}

// Infraestructura: el adaptador se cambia sin tocar el dominio.
final readonly class DoctrineOrderRepository implements OrderRepository
{
    public function __construct(private EntityManagerInterface $em) {}

    public function get(OrderId $id): Order
    {
        return $this->em->find(Order::class, $id)
            ?? throw OrderNotFound::withId($id);
    }

    public function save(Order $order): void
    {
        $this->em->persist($order);
        $this->em->flush();
    }
}

Es la recompensa de todo lo anterior: las reglas se prueban en milisegundos, sin levantar nada.

OrderTest.php

final class OrderTest extends TestCase
{
    public function test_a_shipped_order_cannot_be_cancelled(): void
    {
        $order = OrderMother::shipped();

        $this->expectException(OrderAlreadyShipped::class);

        $order->cancel(new DateTimeImmutable('2026-01-01'));
    }

    public function test_cancelling_twice_records_one_event(): void
    {
        $order = OrderMother::paid();
        $now = new DateTimeImmutable('2026-01-01');

        $order->cancel($now);
        $order->cancel($now);

        $this->assertCount(1, $order->releaseEvents());
    }
}

Ejemplo ilustrativo, simplificado para esta página.

Criterio

Reintentar también puede tumbarte.

Una caída y cinco formas de reaccionar. Esto no es un vídeo: el modelo se ejecuta en tu navegador con cada cambio. Prueba a alargar la caída y mira cuándo deja de recuperarse.

Estrategia de reintentos
8 s

Salva mucho más, pero quienes cayeron a la vez vuelven a la vez. Las olas alargan la recuperación.

Peticiones que acaban bien
98,2 %
Carga extra generada
+55 %
Recuperación tras la caída
16 s
AtendidasRechazadasCapacidadCaídapet./s

Ninguna estrategia es gratis. Elegir una depende de qué duela más: perder peticiones o alargar la caída.

Modelo simplificado: 60 pet./s, capacidad de 100 pet./s, hasta 5 intentos y rendimiento que cae bajo sobrecarga.

Escribí sobre reintentos en NestJS

Blog

Lo que aprendo, por escrito.

Ver todos los artículos