Por qué este blog no tiene base de datos
Cada vez que le cuento a alguien del gremio cómo funciona este blog, la conversación llega al mismo punto. Explico que es un servidor en Go, que el contenido son ficheros Markdown, que hay hot reload, un API, un scheduler que publica solo... y entonces cae la pregunta: "¿y qué base de datos usas?". Ninguna. Silencio. "¿SQLite por lo menos?". Tampoco. La única fuente de verdad de este blog es un directorio con ficheros .md dentro, y llevo el suficiente tiempo con él en producción como para defender la decisión por escrito.
No es una postura ideológica ni un ejercicio de minimalismo por el minimalismo. Es que, para este problema concreto, una base de datos no aporta nada y quita bastante. Este post es el recorrido por lo que hay en su lugar, lo que gano, lo que pierdo, y cuándo cambiaría de opinión.
Qué hay en lugar de una base de datos
El flujo completo cabe en un párrafo. Al arrancar, un loader recorre content/posts/ y content/pages/, parsea el front matter YAML de cada .md, convierte el Markdown a HTML con goldmark (con el resaltado de sintaxis de chroma ya resuelto en servidor, sin una línea de JavaScript) y mete el resultado en un store en memoria: dos maps protegidos por un sync.RWMutex, uno para posts y otro para páginas. Todas las peticiones web leen de ahí. Nunca se toca el disco para servir una página.
A partir de ese momento, el disco y la memoria se mantienen sincronizados solos. Un watcher con fsnotify vigila los directorios de contenido; cuando un fichero cambia, espera 100 milisegundos de debounce (los editores disparan ráfagas de eventos al guardar) y recarga solo ese fichero: lo vuelve a parsear, lo convierte y hace un upsert en el store. Guardo un post desde vim y está publicado antes de que me dé tiempo a cambiar de ventana. No hay paso de compilación, no hay caché que invalidar a mano, no hay reinicio.
Cada vez que el contenido cambia, el store recalcula los agregados una sola vez: la lista de posts ordenada por fecha, las categorías y tags con sus contadores para el sidebar, los últimos cinco posts, la navegación. Las peticiones no ordenan ni cuentan nada; leen estructuras ya masticadas. Es la misma idea que un índice de base de datos, solo que el "índice" se reconstruye entero en cada cambio, porque con este volumen de contenido reconstruirlo cuesta menos que pestañear.
Alrededor de ese núcleo hay tres piezas más. Un scheduler que se despierta cada minuto, busca drafts cuya fecha ya ha llegado y los publica: primero reescribe el .md en disco quitando el draft: true (el disco es la fuente de verdad; si publicara solo en memoria, un reinicio despublicaría el post) y después actualiza el store. Este mismo post ha salido así: lo dejé escrito con fecha futura y el scheduler hizo el resto un sábado a las nueve de la mañana. Un contador de visitas que vive en un map en memoria y se persiste a content/views.json una vez por minuto, y solo si hubo cambios. Y la búsqueda, que es un strings.Contains sobre el título y el texto plano de cada post, recorriendo la lista en memoria. Sin Elasticsearch, sin índice invertido, sin nada: para algo más de 150 posts, fuerza bruta en RAM es instantánea.
Qué gano
El deploy es un rsync. Subo el código al servidor, systemd recompila y reinicia, y el contenido ni se entera: son ficheros que ya estaban ahí. No hay migraciones de esquema, no hay ALTER TABLE, no hay versión N-1 del código leyendo la versión N de los datos. En años de blog no he escrito una sola migración, y cada campo nuevo del front matter es opcional por diseño: los posts viejos simplemente no lo tienen.
El backup es un tar. tar -czf backup.tar.gz content/ y tengo el blog entero: posts, páginas, imágenes y contador de visitas. Unos 24 MB. Restaurar es descomprimir. No hay dumps, no hay binlogs, no hay "espera que compruebo que el backup se puede restaurar", porque el backup es el contenido, en un formato que podré leer dentro de veinte años con cualquier editor.
Todo es greppable y versionable. ¿En cuántos posts he mencionado fsnotify? grep -rl fsnotify content/posts/. ¿Qué cambié en un post el mes pasado? git log -p sobre el fichero. El contenido va en git como el código, con diffs legibles y blame. Una base de datos convierte esas preguntas triviales en sesiones de cliente SQL; aquí son herramientas que ya sé usar desde 1998.
Servir desde memoria es absurdamente rápido. El pie de cada página de este blog muestra el tiempo de renderizado, y lo normal es que leas ahí cientos de microsegundos: lo que tarda Go en ejecutar una plantilla sobre structs que ya están en RAM. No hay pool de conexiones, ni latencia de query, ni caché de segundo nivel para compensar. La caché soy yo entero: el dataset completo cabe en memoria con holgura, y ese es exactamente el caso en el que una base de datos deja de tener sentido como capa de lectura.
Y hay un beneficio lateral que no vi venir: como el contenido ya es Markdown, exponerlo para agentes y LLMs fue casi gratis. /llms.txt y /llms-full.txt existen porque el "formato de exportación" es el formato de almacenamiento. Cualquier post de este blog tiene su versión Markdown añadiendo .md a la URL. Prueba con esta misma.
Qué pierdo (y cómo lo sorteo)
Seamos honestos, que de eso va el post. Lo primero que pierdo son las garantías de concurrencia que una base de datos regala: transacciones, aislamiento, escrituras atómicas. Con ficheros hay que ganárselas a mano, y son los dos detalles menos negociables de todo el diseño. Uno: ninguna escritura toca el fichero destino directamente; todo se escribe en un temporal del mismo directorio y luego se renombra, porque el rename es atómico en POSIX y así el watcher (o un proceso que muere a medias) nunca lee un fichero a medio escribir. Dos: las operaciones multi-paso —comprobar que un slug no existe, escribir, renombrar— pasan por un mutex de escrituras global que serializa al API y al scheduler entre sí. Es un lock groserísimo comparado con MVCC, y me da exactamente igual: los escritores de este blog somos yo y un proceso que se despierta cada minuto. Optimizar la contención de un escritor y medio es ingeniería de currículum.
Lo segundo que pierdo son las queries ad hoc. No puedo hacer un GROUP BY arbitrario sobre mis posts... salvo que escriba un bucle en Go, que es lo que son las agregaciones del sidebar y la búsqueda. La realidad es que un blog tiene media docena de preguntas que responder —posts por fecha, por tag, por categoría, texto que contenga algo— y todas están precalculadas o se resuelven recorriendo una lista. Llevo años esperando a necesitar una query que no pueda expresar así. Sigo esperando.
Y lo tercero: nada de datos generados por usuarios. Sin base de datos no hay comentarios, ni cuentas, ni likes. El contador de visitas es lo máximo que me permito, y fíjate en el compromiso que acepta: persiste una vez por minuto, así que un kill -9 en el momento justo puede perder hasta un minuto de visitas. Para un contador decorativo es un trato estupendo; para el carrito de la compra de alguien sería negligencia.
Cuándo sí pondría una base de datos
En el momento en que cualquiera de esas tres renuncias dejara de ser cómoda. Si este blog tuviera comentarios, pondría SQLite ese mismo día, porque datos que escriben desconocidos concurrentemente es justo el problema que las bases de datos resuelven y los ficheros no. Si el contenido creciera hasta no caber en memoria con comodidad, o hasta que recalcular los agregados en cada cambio doliera, también. Si hubiera varios procesos escribiendo desde máquinas distintas, mi mutex de un solo proceso saltaría por los aires y lo sabría antes de intentarlo.
Nada de eso está cerca de pasar. El blog completo son unas 5.900 líneas de Go contando tests, y una parte muy pequeña de ellas es la que reemplaza a la base de datos: un par de maps, un mutex de lectura, otro de escritura y una función que escribe temporales y renombra. No es que haya evitado la complejidad: es que la complejidad era opcional y elegí el lado que puedo depurar con cat.
La pregunta interesante nunca fue "¿qué base de datos uso?" sino "¿qué necesito de verdad?". Y la respuesta, para un blog personal escrito por una persona y leído por muchas, resultó ser: un directorio de ficheros de texto y un proceso que se los sabe de memoria. Literalmente.