Apache Spark está escrito en Scala y eso no es un detalle menor. Significa que la API de Scala es la primera en recibir las novedades, que el tipado es más preciso que en otras APIs del mismo Spark, y que hay cero overhead de interop. Si trabajas con procesamiento de datos a escala, Scala con Spark sigue siendo la combinación de referencia en 2026.
Dataset[T] vs DataFrame: el tipo importa
Spark ofrece dos abstracciones principales para trabajar con datos estructurados: DataFrame y Dataset[T]. La diferencia es el tipado. Un DataFrame es básicamente un Dataset[Row], donde los tipos de columna no se comprueban en compilación. Un Dataset[T] sí es seguro en tiempo de compilación.
import org.apache.spark.sql.SparkSession
case class Venta(producto: String, cantidad: Int, precio: Double)
val spark = SparkSession.builder()
.appName("VentasApp")
.master("local[*]")
.getOrCreate()
import spark.implicits.*
val ventas: Dataset[Venta] = spark.read
.option("header", "true")
.option("inferSchema", "true")
.csv("ventas.csv")
.as[Venta]
val totalPorProducto = ventas
.filter(_.cantidad > 0)
.groupBy("producto")
.agg(Map("precio" -> "sum", "cantidad" -> "sum"))
Usar Dataset[Venta] en lugar de DataFrame hace que el compilador te avise si accedes a un campo que no existe en Venta. El coste es que las operaciones de tipo lambda como .filter(_.cantidad > 0) no se pueden optimizar tan bien como las operaciones SQL del plan lógico de Spark. En la práctica, para lecturas y transformaciones simples, el Dataset tipado es preferible; para agregaciones complejas, mezclar con la API SQL suele dar mejor rendimiento.
Lectura y escritura de formatos
Spark soporta Parquet, ORC, JSON, CSV, Avro y formatos de delta lakes como Delta Lake o Apache Iceberg. Parquet es el formato por defecto para almacenamiento columnar eficiente:
// Leer Parquet
val df = spark.read.parquet("s3a://mi-bucket/datos/")
// Escribir con particionado
df.write
.mode("overwrite")
.partitionBy("año", "mes")
.parquet("s3a://mi-bucket/resultado/")
// Registrar como vista temporal para usar SQL
df.createOrReplaceTempView("ventas")
val top10 = spark.sql("SELECT * FROM ventas ORDER BY precio DESC LIMIT 10")
Spark 3.5: lo más relevante
Spark 3.5, publicado en septiembre de 2023, trajo varias mejoras prácticas. El modo ANSI SQL está activado por defecto, lo que significa que operaciones que antes silenciosamente devolvían null en vez de error ahora lanzan excepción. Es un cambio que puede romper pipelines existentes pero que hace el código más predecible.
Spark Connect es la otra novedad importante: separa el cliente de Spark del servidor, permitiendo conexiones remotas desde cualquier lenguaje sin necesidad de tener el classpath de Spark completo en el cliente. Para equipos donde los científicos de datos usan Python y los ingenieros Scala, esto facilita mucho la convivencia.
El Structured Streaming también mejoró con nuevas funciones de ventana y mejor soporte de stateful operations, que son las más complejas de implementar bien en streaming.
Structured Streaming con Scala
El procesamiento de datos en tiempo real con Spark usa la misma API que el batch, lo que simplifica mucho el código:
val streamingDF = spark.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "broker:9092")
.option("subscribe", "eventos")
.load()
val parsed = streamingDF
.selectExpr("CAST(value AS STRING)")
.as[String]
val query = parsed.writeStream
.outputMode("append")
.format("parquet")
.option("path", "/output/eventos/")
.option("checkpointLocation", "/checkpoints/eventos/")
.start()
query.awaitTermination()
El concepto de checkpointing es fundamental en streaming: guarda el estado del procesamiento para que, si el job se cae, pueda retomar desde donde lo dejó sin perder ni duplicar datos.
Scala vs Python en Spark
Python con PySpark es la opción más popular para data science, pero Scala sigue siendo superior en varios aspectos. Primero, el rendimiento: las UDFs de Python tienen un coste de serialización significativo porque hay que mover datos entre la JVM y el proceso de Python. Con Scala ese overhead no existe. Segundo, el tipado: los errores de tipo en Scala aparecen en compilación, no cuando el job ya lleva horas ejecutando en producción. Tercero, el ecosistema de ingeniería de datos: librerías como las del ecosistema JVM con Spark están mejor integradas en Scala.
Para equipos de ingeniería de datos que priorizan la robustez y el rendimiento sobre la velocidad de prototipado, Scala con Spark sigue siendo la elección más sólida. Para análisis exploratorio y ciencia de datos, Python tiene más peso. Muchos equipos usan ambos: ingeniería en Scala, análisis en Python, compartiendo el mismo clúster de Spark.
SBT para proyectos Spark
Un proyecto Spark en Scala típico usa SBT con sbt-assembly para generar un fat JAR que se envía al clúster:
// build.sbt
val sparkVersion = "3.5.0"
libraryDependencies ++= Seq(
"org.apache.spark" %% "spark-core" % sparkVersion % "provided",
"org.apache.spark" %% "spark-sql" % sparkVersion % "provided",
"org.apache.spark" %% "spark-streaming" % sparkVersion % "provided"
)
assembly / assemblyMergeStrategy := {
case PathList("META-INF", _*) => MergeStrategy.discard
case _ => MergeStrategy.first
}
El % "provided" indica que Spark estará en el classpath del clúster y no hay que incluirlo en el JAR final, lo que reduce bastante el tamaño del artefacto.
Imagen: Pexels / alleksana
