CommunityProgramación y desarrollogithub.com

george-andrikopoulos/domain-java

Disciplina de ingeniería para trabajar en Java. Cubre: un camino especulado se desoptimiza cuando la entrada cambia; una vista no es una copia; un tipo envoltorio no es gratuito aquí; cierra lo que abres; diseña para herencia o prohíbelo; la igualdad es un contrato; las excepciones nombran lo que falló; la identidad no es igualdad para cajas; ninguna referencia a internos escapa; null no es un valor; publica de forma segura o no lo hagas; sella las alternativas; Serializable es un segundo constructor.

¿Qué es domain-java?

Este skill, 'domain-java', es una guía de principios de ingeniería para desarrollar software robusto y de alto rendimiento en Java. Proporciona reglas prácticas basadas en el diseño orientado a objetos, la gestión de memoria de la JVM y las mejores prácticas del lenguaje. Está diseñado para desarrolladores que trabajan con Java en cualquier plataforma, incluyendo entornos como Claude Code, Cursor y Codex, y ayuda a prevenir errores sutiles relacionados con colecciones, envoltorios, manejo de recursos, herencia, igualdad y concurrencia. Cada regla incluye un modo de verificación de fallos para identificar problemas y garantizar un código predecible y mantenible.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-java

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

domain: reglas de java

Un camino optimizado con las entradas de ayer se desoptimiza cuando llegan las de hoy [R:un-camino-especulado-se-desoptimiza-cuando-la-entrada-cambia]

Escrito a partir de Aleksey Shipilev, "JVM Anatomy Quarks", sobre contaminación de perfiles, inline y desoptimización; el mecanismo de uncommon-trap de HotSpot.

Un JIT no compila tu código. Compila una apuesta sobre tu código, y la apuesta se liquida en el momento en que la apuesta deja de ser cierta.

HotSpot compila sobre perfil: este sitio de llamada solo ha visto un tipo de receptor, así que haz inline y protege; esta rama nunca se ha tomado, así que no la emitas; este campo nunca ha sido null, así que omite la comprobación; esta clase nunca se ha cargado, así que asume que no existe subclase. Cada suposición obtiene una guarda, y el camino compilado es rápido precisamente porque las guardas son baratas y los cuerpos están ausentes. Cuando una guarda falla, el marco se desoptimiza: una trampa poco común (uncommon trap), los marcos del intérprete se reconstruyen a partir de los compilados, la ejecución continúa interpretada, y se espera para ser recompilada con el nuevo perfil.

El momento es lo que convierte esto en una regla de latencia en lugar de rendimiento. Las suposiciones se rompen cuando la entrada cambia: el tipo de orden inusual, la primera excepción del día, el formato de mensaje de un nuevo lugar, la segunda implementación de una interfaz cargada a la hora seis. Así que la peor latencia que el camino producirá se entrega en el evento menos ordinario, que generalmente es el evento que más importaba. Las pruebas de carga con tráfico representativo-promedio no reproducen nada de esto.

También es silencioso por defecto. No se registra nada, ninguna métrica se mueve, y la única traza es un valor atípico de latencia indistinguible de una pausa de recolección o un retraso de planificación. -XX:+PrintCompilation y -XX:+TraceDeoptimization dicen lo que sucedió; JFR transporta los eventos con menos sobrecarga y es el que vale la pena dejar encendido.

Así que calienta el camino con los casos inusuales además de los comunes, que es donde esto se encuentra con [R:una-medicion-coincide-con-el-regimen-que-reporta]: un calentamiento construido solo con tráfico típico produce un perfil que el tráfico típico confirma y el tráfico atípico destruye. Mantén los sitios de llamada calientes deliberadamente monomórficos; una llamada a interfaz megamórfica en el camino caliente es una barrera de inline permanente en lugar de una trampa única. Carga las clases que el camino necesitará antes de que el camino esté activo. Y trata una lambda o un sitio de llamada dinámico agregado a un camino caliente como un nuevo perfil que debe ganarse de nuevo.

Modo de verificación de fallos: ¿qué ha asumido el JIT sobre este camino, y qué sucede la primera vez que una de esas suposiciones es falsa? Si la respuesta honesta es que nadie sabe lo que se asumió, ese es el hallazgo: las suposiciones son legibles, y leerlas es el trabajo.

Una vista de colección es una ventana a los datos de alguien más [R:una-vista-no-es-una-copia]

Sepa si está sosteniendo datos o una ventana a los datos. El tipo estático es List<T> de cualquier manera y nunca te lo dirá.

La biblioteca devuelve vistas con mucha más frecuencia de lo que la mayoría del código asume, y cada una tiene su propia forma de sorprender:

  • Arrays.asList(a) es un envoltorio de tamaño fijo sobre el array. set escribe a través del array; add y remove lanzan UnsupportedOperationException. List.of(...) es inmutable e independiente, y generalmente es lo que se quería.
  • list.subList(from, to) es una ventana en vivo. Las escrituras pasan, y cualquier cambio estructural en el padre hace que la sublista lance ConcurrentModificationException en su próximo uso, incluyendo el modismo común de tomar una sublista y luego limpiar al padre.
  • map.keySet(), values(), entrySet() son vistas. keySet().remove(k) elimina la entrada del mapa. Eso es ocasionalmente lo que quieres y siempre vale la pena ser deliberado al respecto.
  • Collections.unmodifiableList(l) envuelve sin copiar: el envoltorio rechaza escrituras y el original aún funciona, por lo que defiende contra el llamante y no contra ti. No es una copia defensiva, por lo que [R:ninguna-referencia-a-internos-escapara] recurre a List.copyOf en su lugar.
  • Stream.toList() es inmodificable; collect(Collectors.toList()) no ofrece tal garantía y puede serlo o no. No confíes en que ninguno sea mutable.

Así que indique cuál quiere en el punto en que lo crea. List.copyOf cuando el receptor debe ser independiente, la vista cuando compartir es la intención y la vida útil es corta. Una vista almacenada en un campo es la forma que hay que mirar con más atención: sobrevive a la expresión que la creó, y la colección subyacente puede cambiar debajo de ella en cualquier momento posterior.

La misma distinción gobierna la iteración. Modificar una colección mientras se itera lanza ConcurrentModificationException incluso en un solo hilo, porque el iterador también es una vista. Usa removeIf, o itera una copia, o recoge las eliminaciones y aplícalas después.

Modo de verificación de fallos, para cualquier colección recibida o devuelta: si alguien escribe en esto, ¿qué más cambia? Si no puedes responder sin abrir el Javadoc del método de fábrica, cópialo.

Un tipo envoltorio cuesta un objeto hasta que el análisis de escape lo elimina [R:un-tipo-envoltorio-no-es-gratuito-aqui]

Dale a los conceptos de dominio sus propios tipos aquí también: un OrderId que no se puede pasar donde pertenece un AccountId vale tanto en Java como en cualquier otro lugar. Pero no traslades el argumento de costo con el argumento de diseño, porque solo uno de ellos viaja.

En Rust, un newtype es una construcción en tiempo de compilación: struct OrderId(u64) es un u64 en tiempo de ejecución y el compilador lo prueba. En Java, record OrderId(long value) es un objeto: un encabezado, un campo, una referencia a rastrear y una línea de caché separada de lo que apuntaba a él. El JIT puede eliminarlo: el análisis de escape más la sustitución escalar aplanarán un envoltorio que demostrablemente no escapa de su unidad de compilación, lo que cubre una gran cantidad de código ordinario. Lo que no cubre es el caso que te importa: un envoltorio almacenado en un campo, puesto en una colección, pasado a través de un método que el JIT se negó a incluir en línea, o en un array, donde OrderId[] es un array de punteros y long[] es un array de números.

Así que la disciplina se divide según dónde vive el valor:

  • En un límite de API, y en cualquier lugar donde la corrección sea la preocupación, usa el envoltorio. El error de argumento intercambiado que previene vale una asignación que el JIT normalmente eliminará de todos modos.
  • Dentro de un bucle caliente, en un array grande, o en una estructura por mensaje, mide antes de asumir que el envoltorio desapareció. Esto es [R:verifica-que-la-abstraccion-se-compilo] llegando en un lenguaje donde la decisión del optimizador es dinámica: -XX:+PrintInlining, un perfil de asignación de JFR, o un volcado de análisis de escape dice lo que realmente sucedió, y la respuesta puede diferir entre ejecuciones del mismo binario.
  • Donde no desaparece y el presupuesto es real, mantén el primitivo y pon la seguridad en algún lugar que no cueste nada: un parámetro con nombre, un constructor, una fábrica estática cuya firma no se pueda llamar incorrectamente.

La versión de esta regla que está por venir vale la pena conocerla en lugar de esperarla: las clases de valor de Project Valhalla están diseñadas para hacer exactamente que este envoltorio se aplane por especificación en lugar de por discreción del optimizador. Hasta que el código se ejecute en una JVM donde eso sea cierto, el costo es dinámico y debe medirse en lugar de asumirse.

Modo de verificación de fallos, antes de envolver un primitivo en un camino con un presupuesto: ¿este objeto todavía existe después de que el JIT haya terminado, y qué me lo dijo? "Es solo un envoltorio" es la respuesta de Rust, y es la que no se transfiere.

Un recurso es cerrado por la construcción que lo abrió, en cada camino [R:cierra-lo-que-abres]

Abre un recurso en un encabezado try-with-resources y en ningún otro lugar. La construcción cierra en cada salida, en orden inverso, y (la parte que un finally escrito a mano no puede hacer) mantiene la excepción original cuando el cierre también falla.

try (var conn = pool.take(); var stmt = conn.prepare(sql)) {
    return stmt.execute();
}

El fallo que introduce un bloque finally vale la pena enunciarlo claramente porque parece correcto. Si el cuerpo lanza y close() también lanza, la excepción de finally reemplaza la del cuerpo: el programa informa una falla al cerrar una conexión y no dice nada sobre la consulta que falló primero. Try-with-resources, en cambio, suprime el fallo de cierre y lo adjunta al original, por lo que ambos sobreviven y la causa es la que querías. Una fuga te cuesta un manejador; una causa perdida te cuesta la investigación.

Tres casos donde la construcción no aplica y la disciplina aún lo hace:

  • Un recurso cuya vida útil es un campo, no un bloque (un pool, un cliente, un ejecutor). El objeto propietario se convierte en AutoCloseable y la disciplina sube un nivel; lo que hay que rechazar es un campo que se abre y nunca es cerrado por nadie en particular.
  • Un ExecutorService no se cierra solo con shutdown(). shutdown() luego awaitTermination() luego shutdownNow() es la secuencia, y saltarla deja hilos no daemon sosteniendo la JVM.
  • Un Stream sobre un archivo (Files.lines, Files.walk) tiene un manejador y debe cerrarse. Es el único stream que filtra, y se ve exactamente como los que no lo hacen.

No recurras a un finalizador o Cleaner como mecanismo principal. Se ejecutan en un momento no especificado o nunca, y existen para detectar el caso donde la disciplina ya falló.

Modo de verificación de fallos, para cada recurso: ¿qué construcción cierra esto si la siguiente línea lanza? Si la respuesta es una llamada close() más abajo en el método, nada lo hace.

Una clase es final, o su herencia está diseñada y documentada [R:disena-para-herencia-o-prohibela]

Haz la clase final a menos que hayas diseñado para una subclase, y si lo has hecho, di exactamente qué puede sobrescribir una subclase y qué no debe.

La herencia no es el mecanismo de reutilización que parece. Una subclase depende de qué métodos de la superclase llaman a otros: addAll llamando a add es el ejemplo canónico, y eso es un detalle de implementación que la superclase tiene derecho a cambiar en un parche de lanzamiento. Así que una subclase funcional se rompe cuando el padre es refactorizado, sin que ninguno de los autores haga algo mal. La composición no tiene esta propiedad: un envoltorio depende del contrato público y nada más.

El caso del constructor es peor que frágil, está roto desde el principio. Un constructor que llama a un método sobrescribible ejecuta la sobrescritura de la subclase antes de que los campos propios de la subclase sean asignados, por lo que la sobrescritura ve null y 0 en campos que declaró final e inicializó. Nada advierte. Este es el mismo peligro de publicación que [R:ninguna-referencia-a-internos-escapara] nombra como this escapando, alcanzado por una ruta que parece un despacho de método ordinario, y [R:publica-de-forma-segura-o-no-lo-hagas] es por qué importa incluso cuando solo hay un hilo.

Así que:

  • final por defecto en clases, y en cualquier método que una subclase no tenga por qué reemplazar. El sellado ([R:sella-las-alternativas]) es la forma más fuerte donde el conjunto de subtipos está cerrado y es conocido.
  • Nunca llames a un método sobrescribible desde un constructor, un inicializador, o clone() o readObject(), que son constructores con otros nombres.
  • Si una clase está diseñada para herencia, documenta el autouso: qué métodos llaman a cuáles, cuáles son seguros de sobrescribir, qué debe llamar una subclase. Esa documentación es parte del contrato, y el costo de ella es la razón honesta por la que la mayoría de las clases deberían ser final en su lugar.
  • Prefiere la composición. Un envoltorio que contiene la instancia y reenvía es más largo de escribir, inmune a los internos del padre, y es lo que hay que buscar cuando la motivación fue la reutilización en lugar de la sustituibilidad.

Modo de verificación de fallos, para cada clase no final: ¿cuáles de mis métodos se llaman entre sí, y una subclase que sobrescriba uno de ellos seguiría siendo correcta después de que los reordene? Si eso no se puede responder, la clase fue extensible por accidente.

equals y hashCode son una decisión, y una clave mutable rompe ambos [R:la-igualdad-es-un-contrato]

Decide la igualdad una vez, implementa ambas mitades en el mismo cambio, y derívalas de campos que no pueden cambiar mientras el objeto sea accesible desde una colección.

El contrato es mecánico: objetos iguales deben tener códigos hash iguales. Rómpelo y un HashMap mira en el cubo incorrecto, por lo que get devuelve null para una clave que contiene y remove no puede encontrar una entrada que eliminar: una fuga cuyo tamaño crece con el tráfico. Nada detecta esto. La colección se está comportando exactamente como se especificó; la especificación recibió un objeto que mintió.

La parte de la clave mutable es la que sobrevive a la revisión, porque la clase es correcta cuando se escribe y se vuelve incorrecta cuando alguien agrega un setter. Un objeto cuyo hashCode depende de un campo que luego se muta se ha movido de cubo sin que el mapa lo sepa, y ahora es inalcanzable a través de la misma clave bajo la que está archivado. Así que un tipo usado como clave es inmutable, o no se usa como clave. Un record te da ambas mitades generadas a partir de los componentes y campos finales por construcción, por lo que es la forma predeterminada para un tipo de valor y por qué escribir estos dos métodos a mano debería sentirse como una decisión en lugar de código repetitivo.

Dos más que se derivan del mismo contrato y vale la pena indicarlos porque cada uno tiene su propia forma de salir mal:

  • compareTo debe concordar con equals en cualquier lugar donde el tipo llegue a un TreeMap o un conjunto ordenado, que usan ordenamiento y nunca llaman a equals. Un comparador que dice que dos objetos son equivalentes mientras equals dice que difieren produce un conjunto que silenciosamente contiene uno de ellos.
  • La herencia y la igualdad no se componen. Un equals escrito con instanceof acepta una subclase y luego es asimétrico; uno escrito con getClass() es simétrico y rechaza cada subclase. No hay una tercera opción, que es una de las razones por las que [R:disena-para-herencia-o-prohibela] prefiere final.

Modo de verificación de fallos, antes de que un tipo se use como clave o se ponga en un conjunto: ¿de qué campos depende su igualdad y su código hash? Si alguno de ellos tiene un setter público, entonces el tipo no es una clave y no debe usarse como tal.

Las excepciones nombran lo que falló, no dónde falló [R:las-excepciones-nombran-lo-que-fallo]

Lanza lo que significa: un IllegalArgumentException cuando un argumento no es válido, no un RuntimeException genérico. El nombre es parte de la señal, y el receptor de la excepción hace una elección basada en el tipo. Una excepción comprobada es una garantía de que el llamante se ocupe de ella; una no comprobada es una garantía de que no lo hará.

Dos fallos que merecen sus propias excepciones en lugar de una genérica:

  • NullPointerException para un argumento que es null cuando no debería serlo, o un estado interno que es null cuando no debería serlo. No lo envuelvas en otra excepción: es la señal más clara.
  • IllegalStateException para un método llamado en un momento inapropiado, como llamar a next() en un iterador sin verificar hasNext() primero.

Y no uses una excepción para el flujo de control. El costo de construir, lanzar y capturar una excepción es alto y, peor aún, el perfil de rendimiento es no determinista: el JIT puede optar por no compilar un camino caliente con una captura de excepción.

Modo de verificación de fallos, para cada método: cuando falla, ¿el tipo de excepción le dice al llamante qué fue lo que falló? Si no, el nombre es la primera cosa que arreglar.

La identidad no es igualdad para cajas, y nunca lo fue [R:la-identidad-no-es-igualdad-para-cajas]

Compara objetos de valor con .equals(), no con ==. El == verifica la identidad de la referencia, no la igualdad del valor, y para las clases envoltorio (Integer, Long, Boolean, etc.) el resultado depende de si el compilador o el JIT eligió almacenar en caché o no.

Integer.valueOf(100) == Integer.valueOf(100) es true; Integer.valueOf(200) == Integer.valueOf(200) es false, porque el caché por defecto solo va hasta 127. new Integer(100) == new Integer(100) es false siempre, porque new siempre crea un nuevo objeto. Todo esto es comportamiento especificado y conocido, y todo es una mala noticia que espera suceder.

Los registros (records) lo hacen explícito: record Punto(int x, int y) {} y new Punto(1, 2).equals(new Punto(1, 2)) es true; == es false y nadie se sorprende. El problema es que las clases envoltorio existentes se comportan como registros sin serlo, y == no es seguro para ninguno de ellos.

La regla es simple: == es para primitivos y enums, .equals() para todo lo demás. No confíes en el caché.

Ninguna referencia a internos escapa [R:ninguna-referencia-a-internos-escapara]

No devuelvas una referencia a un objeto mutable que pertenezca al estado interno del objeto. Cópialo o hazlo inmutable.

El caso clásico es un array o una colección mutable devuelto por un getter. El llamante recibe una referencia al interno y puede mutarlo, violando el invariante del objeto. La solución es List.copyOf para colecciones, o Arrays.copyOf para arrays, o un envoltorio inmutable.

Lo mismo se aplica a los constructores: si almacenas una referencia a un objeto mutable pasado como parámetro, el llamante aún tiene una referencia y puede mutar el objeto después de que el constructor haya terminado. Cópialo en la entrada también.

El caso del constructor es peor: si this escapa antes de que el constructor termine, otro hilo puede ver un objeto parcialmente construido. Esto es un peligro de publicación ([R:publica-de-forma-segura-o-no-lo-hagas]) y es la razón por la que no se debe llamar a métodos sobrescribibles desde un constructor.

Modo de verificación de fallos, para cada método que devuelve un objeto: si el llamante muta la cosa devuelta, ¿el estado de este objeto sigue siendo correcto? Si no, cópialo.

null no es un valor [R:null-no-es-un-valor]

null no es un argumento válido, no es un valor de retorno válido, no es un elemento de colección válido. Es un error en espera de ocurrir, y el compilador no te ayudará a encontrarlo.

java.util.Optional es la alternativa para un valor que puede estar ausente. Un Optional vacío es un valor que significa "sin valor", y el receptor debe decidir cómo manejarlo. Un null es un puntero que espera ser desreferenciado, y la decisión se toma en el lugar de la desreferencia, que puede estar muy lejos de la fuente.

Lo mismo se aplica a los parámetros: si un método no acepta null, documéntalo con @NonNull o @NotNull y considera lanzar NullPointerException si se pasa null. Mejor aún, usa java.util.Objects.requireNonNull al inicio del método.

Modo de verificación de fallos, para cada método: ¿puede este método devolver null? Si la respuesta es sí, el llamante debe ser forzado a manejarlo. Si la respuesta es no, el tipo de retorno debería hacerlo imposible.

Publica de forma segura o no lo hagas [R:publica-de-forma-segura-o-no-lo-hagas]

Un objeto puede ser publicado de forma insegura si se hace visible para otro hilo sin las garantías de visibilidad adecuadas. Esto significa que el otro hilo puede ver un objeto parcialmente construido, o valores de campo obsoletos.

La forma segura de publicar:

  • Inicialización estática: private static final MiObjeto INSTANCE = new MiObjeto(); es segura porque la inicialización estática está garantizada por el JLS.
  • Campo volatile: private volatile MiObjeto campo; asegura que cualquier hilo que lea campo vea el valor más reciente.
  • Campo final: private final MiObjeto campo; asegura que el valor sea seguro para su publicación después de que el constructor termine.
  • Colección sincronizada: usar Collections.synchronizedMap o ConcurrentHashMap.
  • Técnicas de bloqueo: usar synchronized o ReentrantLock.

El caso común de publicación insegura es un campo que se escribe sin sincronización y se lee desde otro hilo. R:ninguna-referencia-a-internos-escapara es una instancia de esto: un objeto cuyo constructor deja escapar this puede ser visto por otro hilo antes de que el constructor termine.

Modo de verificación de fallos, para cada objeto que es accedido por más de un hilo: ¿cómo se publicó? Si la respuesta es "a través de un setter sin sincronización" o "a través de un constructor que dejó escapar this", entonces no se publicó de forma segura.

Sella las alternativas [R:sella-las-alternativas]

Cuando un tipo tiene un conjunto conocido y cerrado de subtipos, decláralo sellado (sealed). Esto le dice al compilador exactamente qué subtipos existen, y le permite al compilador verificar que un switch sobre el tipo sea exhaustivo.

sealed interface Forma permits Circulo, Rectangulo, Triangulo {}
final class Circulo implements Forma {}
final class Rectangulo implements Forma {}
final class Triangulo implements Forma {}

El compilador sabe que solo hay tres implementaciones, por lo que un switch sobre Forma sin un caso por defecto o sin manejar las tres variantes es un error de compilación. Esto es más fuerte que R:disena-para-herencia-o-prohibela porque el conjunto de subtipos está cerrado y es conocido.

R:seal-the-alternatives es la versión de Java de una unión discriminada (ADT). Cuando el conjunto de variantes es fijo, declararlo sellado hace que el código sea más seguro y la intención más clara.

Modo de verificación de fallos, para cada interfaz o clase abstracta: ¿puedo enumerar todas las implementaciones de esta interfaz? Si la respuesta es sí y el conjunto es pequeño, hazlo sellado.

Serializable es un segundo constructor y debe ser tratado como tal [R:serializable-es-un-segundo-constructor]

Serializable crea una segunda forma de construir una instancia de su clase, sin pasar por el constructor normal. El flujo de entrada de deserialización usa el constructor sin argumentos de la primera clase no serializable en la jerarquía, o, si la clase implementa Serializable directamente, no usa ningún constructor en absoluto. Esto significa que cualquier invariante establecido por el constructor normal puede ser eludido.

Para controlar esto:

  • Implementa readObject para validar el estado después de la deserialización, como lo haría un constructor.
  • Implementa writeReplace y readResolve para controlar qué objeto se serializa o deserializa realmente.
  • Nunca confíes en que los campos tengan ciertos valores después de la deserialización.
  • No confíes en que final en un campo signifique que no puede cambiar después de la deserialización; la reflexión y la deserialización pueden eludir la finalidad.

R:disena-para-herencia-o-prohibela se aplica aquí también: una clase diseñada para herencia que también implementa Serializable es doblemente peligrosa, porque la subclase puede ser deserializada para crear una instancia sin pasar por el constructor de la superclase.

Modo de verificación de fallos, para cada clase serializable: si alguien construye una instancia de esta clase a partir de un flujo de bytes arbitrario, ¿el estado de la instancia sigue siendo válido? Si no, implementa readObject para validar el estado.

Una medición coincide con el régimen que informa [R:una-medicion-coincide-con-el-regimen-que-reporta]

Mide en las condiciones bajo las cuales opera el sistema. Una prueba de rendimiento que ejecuta un solo hilo en un bucle caliente no te dice cómo se comporta el sistema bajo carga concurrente real. Una prueba que ejecuta un promedio de tráfico típico no te dice cómo se comporta el sistema cuando llega una ráfaga de solicitudes.

Esto se aplica a:

  • Régimen de carga: mide bajo la carga esperada, no bajo carga ligera. El rendimiento y la latencia cambian con la carga.
  • Perfil de entrada: mide con datos que coinciden con el perfil de producción. R:un-camino-especulado-se-desoptimiza-cuando-la-entrada-cambia es una consecuencia de esto.
  • Configuración de la JVM: mide con las mismas banderas de JVM y el mismo recolector de basura que en producción. Diferentes configuraciones pueden producir resultados dramáticamente diferentes.
  • Estado de calentamiento: mide después de que la JVM se haya calentado, no durante el calentamiento.

Modo de verificación de fallos, para cada prueba de rendimiento: ¿bajo qué condiciones se tomó esta medición? Si las condiciones no coinciden con la producción, la medición no es útil.

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-low-latency

Engineering discipline for working in low-latency. Covers: attack the design in a second pass; a measurement matches the regime it reports; a queue without a bound has no overload behaviour; a structure keeps the regime it was proved under; allocated is not resident; answer the requirement at its layer; no allocation on the hot path; no coordinated omission; no false sharing on a hot line; no retry loop on a contended path; no stall inside a publication window; no syscall on a bounded path;...

george-andrikopoulos/domain-rust

Engineering discipline for working in rust. Covers: design types first; borrow in signatures; module visibility is deliberate; must use on consequential returns; newtype liberally; parse wide then range check; seal closed trait sets; typestate builder for required fields; typestate for protocols; verify the abstraction compiled away; async all the way; errors name what failed; justify every clone; no let underscore on a local; parse dont validate; private fields only; xplat fixtures; no...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

Skills relacionados