Skip to content
View velezanthony's full-sized avatar

Block or report velezanthony

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Content in all repositories owned by your account will be closed.
Maximum 250 characters. Please don’t include any personal information such as legal names or email addresses. Markdown is supported. This note will only be visible to you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
velezanthony/README.md

👋 Hola, soy Anthony Velez Tapia

Desarrollador Full Stack — Django · Python · TypeScript · AWS

Full stack con Django en SSR: back y front son el mismo trabajo. Las interfaces las hago con Cotton, HTMX, Alpine y Tailwind; debajo, PostgreSQL y Docker, y AWS cuando el proyecto lo pide. Ahora mismo, sobre una plataforma SaaS en producción.

No siempre fue Django. Antes vinieron .NET, Laravel y Angular, y ver el mismo problema resuelto en ecosistemas distintos es de donde sale casi todo lo que sé.

De ahí salen también las herramientas que construyo por mi cuenta, para que escribir interfaces en Django duela menos. Una de ellas ya está en PyPI.

Toda la trayectoria →


Lo que me ha enseñado producción

Todo lo que sé lo he aprendido resolviendo problemas, o de la gente que me he ido encontrando por el camino.

El cuello de botella casi nunca es la base de datos

Antes de tocar nada, mido: casi nunca es lento lo que yo daba por hecho. Y cuando algo va lento, la respuesta no es cambiar de arquitectura, es averiguar qué falla dentro del stack que ya tienes montado.

Esa búsqueda empieza por las N+1. Cada consulta suelta es rapidísima, décimas de milisegundo, pero el tiempo no se va en la velocidad de respuesta, que es lo único que solemos mirar. Se va en repetir el viaje doscientas veces: la latencia de red, planificar la consulta otra vez, deserializar la respuesta y construir el objeto en el ORM. Todo eso, una vez por fila y en serie.

sequenceDiagram
    participant A as Tu aplicación
    participant D as PostgreSQL

    Note over A,D: N+1 — una consulta, y luego otra por cada fila
    A->>D: SELECT * FROM pedidos
    D-->>A: 200 filas
    loop 200 veces
        A->>D: SELECT * FROM clientes WHERE id = ?
        D-->>A: 1 fila
    end
    Note over A,D: 201 viajes. Cada uno rapidísimo. Todos juntos, no.

    Note over A,D: El mismo dato, resuelto en el servidor
    A->>D: SELECT ... FROM pedidos JOIN clientes
    D-->>A: 200 filas
    Note over A,D: 1 viaje
Loading

A partir de ahí ataco siempre en el mismo orden:

  1. Quitar las N+1, que explican la mayor parte del tiempo perdido.
  2. Paginar de verdad en el backend, y no recortar en memoria una lista que ya me he traído entera.
  3. Delegar los agregados a la base de datos en lugar de arrastrar media tabla hasta el backend para sumarla allí. Da igual que sea Django, .NET o Next: la base de datos lo hace mejor y sin comerse la RAM.
  4. Y ya por último, mirar la base de datos: índices y normalización.

El orden no va de lo fácil a lo difícil: los tres primeros pasos borran trabajo y el cuarto solo abarata el que queda. Y si empiezas por el final, un N+1 bien indexado sigue haciendo doscientos viajes mientras tú ya das la aplicación por optimizada.

Más capacidad no siempre significa más rendimiento

Un pool de mil conexiones parece que va a aguantar más que uno de veinte, y ocurre lo contrario. El número no sale de la nada: el wiki de PostgreSQL lo ata a la máquina que corre la base de datos, núcleos × 2 más un margen por disco. Con cuatro núcleos rondas las nueve conexiones; con ocho, las diecisiete.

Como PostgreSQL levanta un proceso por conexión, pasarse no da más capacidad: da más procesos peleándose por los mismos núcleos. La cola no desaparece al ampliar el pool, solo se muda de sitio — y esperar dentro de PostgreSQL sale mucho más caro que esperar fuera.

flowchart LR
    R(["500 peticiones a la vez"])

    R --> A[["Pool de 20<br/>la cola espera aquí fuera"]]
    R --> B[["Pool de 1000<br/>pasan casi todas"]]

    A --> DA[("PostgreSQL<br/>20 procesos<br/>sin estorbarse")]
    B --> DB[("PostgreSQL<br/>1000 procesos<br/>por 8 núcleos")]

    DA --> OK("Terminan antes")
    DB --> KO("Terminan después")
Loading

El caso más citado, que recoge la documentación de HikariCP, es el del grupo de rendimiento de Oracle: bajaron un pool de 2048 conexiones a 96, sin tocar nada más, y el tiempo de respuesta pasó de unos 100 ms a unos 2 ms. Es de las cosas que más me ha costado aceptar, porque va justo en contra de lo que dice la intuición.

La complejidad hay que ganársela

Veo muchas ofertas pidiendo microservicios, y últimamente también microfrontends. Y cada vez me hago las mismas preguntas: ¿de verdad hay tantas empresas que necesiten separar tanto, o es que toca? ¿Y tienen el equipo para sostenerlo?

Quiero ser justo con la comparación, porque rara vez lo es: Redis, Celery, un load balancer o Sentry no son el precio de los microservicios, son el precio de tener una aplicación seria en producción, y un monolito acaba usándolos igual. Cargárselos a una sola columna es hacer trampa. Lo que de verdad aparece solo al separar es esto:

flowchart TB
    subgraph MONO["Django SSR en un monorepo"]
        M1[["Una aplicación"]]
        MD[("Base de datos")]
        MN("Una transacción que se deshace sola si falla<br/>Un stack trace con la causa entera<br/>El cambio y quien lo consume, en el mismo commit")

        M1 --> MD
        M1 -.- MN
    end

    subgraph MICRO["Microservicios"]
        N1[["Servicio A"]]
        D1[("Su base de datos,<br/>privada")]
        N2[["Servicio B"]]
        D2[("Su base de datos,<br/>privada")]
        NN("¿Dónde se perdió esta petición?<br/>Si falla a medias, lo deshaces tú<br/>Los datos tardan en cuadrar<br/>Reintentar sin cobrar dos veces<br/>Cada repo con su versión")

        N1 --> D1
        N2 --> D2
        N1 <-->|"cambias el endpoint<br/>y quien lo consume<br/>no se entera"| N2
        N2 -.- NN
    end
Loading

El contrato del medio es lo que más me preocupa. Dentro de un monolito, el dato y el código que lo consume viven en el mismo commit: el diff te obliga a tocar los dos lados a la vez. En cuanto ese dato viaja por una API, el mapeo está escrito dos veces en dos repositorios que se despliegan por separado, y nadie te avisa cuando dejan de coincidir. Te enteras en producción.

Con las SPAs pasa lo mismo en pequeño: dos aplicaciones, dos despliegues y la validación escrita dos veces. Si sois dos personas en la empresa, eso cuesta más de lo que devuelve.

Cuándo sí, y lo digo con Angular y React a mis espaldas: cuando el front tiene estado propio de verdad —un editor, un panel en tiempo real, algo que funcione sin conexión— la SPA se paga sola. Y separar en servicios sale a cuenta cuando una pieza tiene necesidades distintas al resto, por escala o por aislamiento. Lo que discuto no es que existan, es que sean el punto de partida.

El síndrome de las manos rápidas

Escribo deprisa y me como letras más de lo que me gustaría reconocer. Por eso uso tipado siempre que el lenguaje me deje, y pongo linters y type checkers desde el primer día: ruff, mypy y pyright. No es disciplina, es delegar en la máquina lo que la máquina hace mejor que yo.

Los tests van en la misma línea, sabiendo lo que dan de sí: no garantizan que el código esté bien, solo lo acercan a ser estable. Pasarse también se paga, porque un proyecto sobre-testeado se vuelve lento de cambiar, y ese punto medio todavía lo estoy buscando.


Tooling para el front de Django

Renderizar en servidor está muy bien hasta que abres el editor, porque no conoce tus componentes ni tus modelos. Ahí es donde ando cuando nadie me paga por ello.

flowchart LR
    C(["Tu proyecto<br/>Django + Cotton"])

    C -->|en el editor| P[["django-cotton-props<br/>IntelliSense en VS Code"]]
    C -->|en el editor| L[["django-language-service<br/>LSP + diagrama ER"]]
    C -->|en el navegador| G[["django-cotton-gallery<br/>Playground · a11y · tokens"]]
Loading

VS Code

Los componentes de Cotton no tienen contrato. Te equivocas al escribir un prop y no falla nada: renderiza vacío, y te enteras en el navegador media hora más tarde. Esta extensión trae ese error al editor, subrayado mientras escribes, y te pone los props del componente en el autocompletado.

Podría haber sido un linter de línea de comandos, y habría sido bastante menos trabajo. Pero un fallo de plantilla se arregla mejor donde se escribe que en la salida de un comando cinco minutos después, y el autocompletado solo existe si vives dentro del editor.

TypeScript · VS Code API · Django Templates

PyPI Docs

En Django hay alternativas para esto —django-pattern-library es la más sólida, y la usa Wagtail— pero ninguna está pensada para componentes de Cotton, así que para ver uno aislado yo acababa montando una vista de pruebas y borrándola después. Esto es esa vista, pero permanente, con tus design tokens y con los chequeos de accesibilidad, que son lo primero que se queda fuera cuando hay prisa.

Adaptar Storybook era posible: tiene un renderer de servidor, y torchbox/storybook-django ya demuestra el patrón. Pero implica un proceso de Node al lado de Django, un endpoint que renderice cada componente y mockear el contexto a mano. Preferí que los renderice el motor de plantillas de Django, con tu contexto y tu configuración, sin una segunda infraestructura que mantener.

Django · HTMX · Alpine.js · CSS tokens

Los plugins de Django Template Language resuelven bien el resaltado, pero no conocen tu proyecto, así que no pueden avisarte de que ese campo no existe. Este parsea models.py, views.py, urls.py y las migraciones para construir el grafo real del proyecto, y de ahí salen el autocompletado que sí acierta y el diagrama ER en vivo.

Con expresiones regulares habría estado listo mucho antes y habría acertado la mitad de las veces. Por eso es un parser: para poder decir "ese campo no existe" hay que saber qué campos hay.

TypeScript · Parsers · AST

Note

🚧 = en desarrollo.


Además

SnakeORM — encadenar relaciones en Python te deja sin tipos a media cadena, y lo habitual es resolverlo con codegen o con un plugin del type checker. Este experimento no necesita ninguna de las dos, porque Truck.maker.nation.name lo resuelven mypy, Pyright y Pylance de forma nativa.

docker-control-center — saber qué contenedores corren y por qué se ha caído uno son cuatro comandos y bastante scroll. Las alternativas gráficas lo arreglan dejándote un proceso encendido todo el día, comiendo RAM para pintar una interfaz que miras dos minutos. Mientras desarrollas esa memoria la quieres en lo que estás compilando, no en un panel. Esto es un único fichero bash: lo ejecutas, miras y se cierra. Docs


Vivo en Gipuzkoa. Trabajo en híbrido, en remoto dentro de España, y también cojo proyectos freelance.

Portfolio LinkedIn PyPI Gists

Popular repositories Loading

  1. django-cotton-gallery django-cotton-gallery Public

    Drop-in playground for Django Cotton component libraries. Live preview, slot IntelliSense, a11y checks, and theming via your design tokens.

    Python 3

  2. snake-orm snake-orm Public

    A Python ORM that shouts instead of guessing: deep typed relationship navigation, PostgreSQL, MySQL and SQLite, no codegen.

    Python 1

  3. django-cotton-props django-cotton-props Public

    🧵 Full VS Code IntelliSense for Django Cotton — autocomplete, hover docs, 21 diagnostics, quick fixes & a component explorer

    TypeScript 1

  4. velezanthony velezanthony Public

  5. django-language-service django-language-service Public

    Django Template Language support + an interactive ER diagram for your models. Indexes models, views, forms, urls and migrations into a project graph.

    TypeScript

  6. docker-control-center docker-control-center Public

    A Docker dashboard for the terminal. One bash file, no daemon, nothing resident.

    Shell