
Graeme FawcettIt's Like Mario Up in Here I met my first pipeline about 25 years ago. It was shaped like...
I met my first pipeline about 25 years ago. It was shaped like a conveyor belt and it was responsible for sorting all of the Norah Jones and Kidz Bop CDs that didn't sell at Wal-Mart that month back into boxes so we could get a refund.
At the time, I was working for a category manager and supplier of music entertainment products as a lead in their returns centre, responsible for maintaining that conveyor belt of a pipeline and the people and systems that fed it. Not much has changed in the last 25 years, I'm still building systems to manage pipelines though now they're more apt to deliver digital products and infrastructure than the physical kind.
I'm still supporting people too, as best I can. Experience gained is wasted, if not shared. Through this series of posts I'm hoping to share with you, the reader and assumed enjoyer of pipelines and flow, how I go about things.
By now, everyone that has worked in the software is familiar with the idea of a pipeline. The backend people have had them for years, BPEL workflows & ESBs last time I did backend work. The data guys got into the act with their drag and drop, code generating ETL tools a decade ago. They fed directly into the ML guys who do the same thing, but at scale.
Anyone who's been doing DevOps/Platform Engineering work in the last decade or so should be familiar with them too. Chained Jenkins jobs begat Jenkinsfiles. Github's Actions and Gitlab's auto-DevOps moved orchestration to the cloud. Tools like Argo pull in the deployment environment and make the end-to-end flow a matter of configuring a YAML template.
In technical terms, these tools are all generally based on some form of a Direct Acyclic Graph (DAG) which is just a fancy way of saying a sequence of steps that can branch and contract, but can never cycle. In practical terms, this generally means that a set of steps that are provided in a structured format (YAML is common) to an engine that executes them in the order provided. In more concrete terms, a "context accumulating DAG" is often implemented, which is just another way of saying "as the thing runs, keep track of what it's doing and write notes".
To demonstrate, we're going to build one. Or rather, we're going to watch one be built.
Meet Nita.
Nita is a platform engineer at Infinity Co (a division of Zero Industries Ltd). It's her first day.
Nita's not sure what a platform engineer is, I'm not sure any of us really are. She has done some automation in previous positions and really loves Ruby for some reason.
Infinity Co is just getting setup. Nita is part of a brand new team and her manager has told her she's free to solve the problems in her tickets her way, as long as she keeps their customers happy.
** KRM-1 : Deployment Pipeline Needed **
Submitted By: probably.ai@infinity.co
Assigned To: nita@infinity.co
Ticket #: KRM-1
I've got an app that need to be in production tomorrow and I've almost got it working.
Can you setup a deployment pipeline for me? It's Java 8 on Springboot.
I've been told it should be Docker or something...
I don't know about any of that stuff, I normally just push the play button in Eclipse and it works.
Can you try to have it ready for the end of the day?
I'd like to get this deployment done first thing in the morning before everyone gets in.
I hate when people are around, it makes me nervous.
Best Regards,
Alice
Nita finishes reading Alice's ticket and looks around for her boss. Monitor's on but no one's home, as usual. And he's not in his chair either. "Guess I'll figure this out on my own then", she thinks to herself as she gets started.
"First, lets see if we can even build this thing", she says to herself as she pulls the code onto her laptop
nita@infinity:~/work/infinite-money > tree
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── infinity
│ ├── Application.java
│ └── DivisionController.java
└── resources
└── application.properties
7 directories, 4 files
"Here goes", she thought as she typed mise use java@8; mvn package into her terminal and started to relax as the package resolution details scrolled by. She always enjoyed this part.
"Invalid engine version", she mumbled reading the error on the screen. She knew it wasn't going to be Java 8. It was 2025, she had at least /hoped/ it wasn't going to be Java 8. She was unsure how Alice had managed to coax Eclipse into running her application, she assumed the work of an LLM was involved somewhere. mise use java@17; mvn package she typed again.
A few moments, and many, many MB later, the build completed and she reviewed what she'd been given:
nita@infinity:~/work/infinite-money > tree
.
├── mise.toml
├── pom.xml
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── infinity
│ │ ├── Application.java
│ │ └── DivisionController.java
│ └── resources
│ └── application.properties
└── target
├── classes
│ ├── application.properties
│ └── com
│ └── infinity
│ ├── Application.class
│ └── DivisionController.class
├── generated-sources
│ └── annotations
├── infinity-service-0.0.1.jar
├── infinity-service-0.0.1.jar.original
├── maven-archiver
│ └── pom.properties
└── maven-status
└── maven-compiler-plugin
└── compile
└── default-compile
├── createdFiles.lst
└── inputFiles.lst
17 directories, 14 files
She was never sure exactly what Springboot needed all that nonsense for, but it had at least finished what it was doing without errors. "Let's see what we're dealing with", she said to her self as she spawned another terminal.
nita@infinity:~/work/infinite-money > java -XX:-UseContainerSupport -jar target/infinity-service-0.0.1.jar &
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.3)
2026-03-10T10:54:12.923-04:00 INFO 16692 --- [ main] com.infinity.Application : Starting Application v0.0.1 using Java 17.0.2 with PID 16692 (/home/gfawcett/working/sprout/app/target/infinity-service-0.0.1.jar started by gfawcett in /home/gfawcett/working/sprout/app)
2026-03-10T10:54:12.927-04:00 INFO 16692 --- [ main] com.infinity.Application : No active profile set, falling back to 1 default profile: "default"
2026-03-10T10:54:14.653-04:00 INFO 16692 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 9999 (http)
2026-03-10T10:54:14.672-04:00 INFO 16692 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-03-10T10:54:14.672-04:00 INFO 16692 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
2026-03-10T10:54:14.725-04:00 INFO 16692 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-03-10T10:54:14.727-04:00 INFO 16692 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1649 ms
2026-03-10T10:54:15.434-04:00 INFO 16692 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
2026-03-10T10:54:15.522-04:00 INFO 16692 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 9999 (http) with context path ''
2026-03-10T10:54:15.548-04:00 INFO 16692 --- [ main] com.infinity.Application : Started Application in 3.322 seconds (process running for 3.995)
With the service up, she cleared her terminal and tested the two endpoints she'd dug out of the code base:
nita@infinity:~/work/infinite-money > http :9999/health
HTTP/1.1 200
Connection: keep-alive
Content-Length: 2
Content-Type: text/plain;charset=UTF-8
Date: Tue, 10 Mar 2026 14:55:17 GMT
Keep-Alive: timeout=60
OK
The health check seemed ok. "Now", she thought, "let's see what all the fuss is about".
nita@infinity:~/work/infinite-money > http :9999/divide?a=1&b=0
HTTP/1.1 200
Connection: keep-alive
Content-Length: 54
Content-Type: text/plain;charset=UTF-8
Date: Tue, 10 Mar 2026 14:55:38 GMT
Keep-Alive: timeout=60
Infinity. You divided by zero. Welcome to the company.
Nita tried not to ask too many questions about what her company did to make money.
With that settled, she closed that terminal and turned to the the next step, packaging.
"Where is he?", she thought again looking over at her boss' still empty chair. His monitor was still glowing away at the end of the line of cubes, for some reason now showing an animated collage of pictures of his toaster oven. "Oh well", she continued feeling the pressure of the ticket bleeding through, "I guess I'll just figure this out too".
She hadn't been given any orientation when she started and there was never anyone else in the row of cubicles that her team had been assigned. Come to think of it, she'd never actually seen all of her team in one place at one time. "I suppose I'll just get on with it, there's a deployment to complete", the motto of the well seasoned platform engineer escaping her lips.
Given the time constraints, and her love of not over complicating things unnecessarily, she decided a simple two stage Docker build would suffice and would save her the trouble of getting mise configured on the build server, wherever that was.
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 9999
ENTRYPOINT ["java", "-jar", "app.jar"]
"There", she thought, "nice and simple". The first stage represented the minimal requirements to get a jar produced and then she'd discard all of the excess and pluck the jar out into its deployment wrapper.
Alice hadn't provided any direction for configuration her application, so Nita had been forced to dig through the code to find what she needed in the application.properties file:
server.port=9999
management.endpoints.web.exposure.include=health
"Here we go", she thought as she pressed enter on her keyboard.
nita@infinity:~/work/infinite-money > docker build -t infinity-service:test
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/
Sending build context to Docker daemon 22.3MB
Step 1/11 : FROM maven:3.9-eclipse-temurin-17 AS build
---> c73f8f3a9e50
Step 2/11 : WORKDIR /app
---> Using cache
---> 79e8ac138dfc
Step 3/11 : COPY pom.xml .
---> Using cache
---> bd0c3f2cf589
Step 4/11 : RUN mvn dependency:go-offline -B
---> Using cache
---> 5be4346e1b1e
Step 5/11 : COPY src ./src
---> b9b32cd1ed75
Step 6/11 : RUN mvn package -DskipTests -B
---> Running in 21e31b085197
...
...
...
INFO] --- spring-boot:3.2.3:repackage (repackage) @ infinity-service ---
[INFO] Replacing main artifact /app/target/infinity-service-0.0.1.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to /app/target/infinity-service-0.0.1.jar.original
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.929 s
[INFO] Finished at: 2026-03-10T15:25:12Z
[INFO] ------------------------------------------------------------------------
---> Removed intermediate container 21e31b085197
---> 0272ee24ac4c
Step 7/11 : FROM eclipse-temurin:17-jre
---> ff692acf8725
Step 8/11 : WORKDIR /app
---> Using cache
---> e53a22d40ef0
Step 9/11 : COPY --from=build /app/target/*.jar app.jar
---> 230b4b03cb4b
Step 10/11 : EXPOSE 9999
---> Running in 21247500458a
---> Removed intermediate container 21247500458a
---> 1be013450fd2
Step 11/11 : ENTRYPOINT ["java", "-jar", "app.jar"]
---> Running in 98b67c550707
---> Removed intermediate container 98b67c550707
---> 1cfa22fed259
Successfully built 1cfa22fed259
Successfully tagged infinity-servce:test
Having built Alice's jar up front, she wasn't surprised that it built again inside of her Dockerfile. She was pleasantly surprised that she hadn't made any typos though. Before she started thinking about deployment models, she ran one final local test.
nita@infinity:~/work/infinite-money > docker run -d --rm -p 9999:9999 infinity-service:test
26c0a30762ffa58836167f4b12792d9a685c5661e66f6b45e4f10290f6e65ba7
nita@infinity:~/work/infinite-money > docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
26c0a30762ff infinity-service:test "java -jar app.jar" 6 seconds ago Up 5 seconds 0.0.0.0:9999->9999/tcp, [::]:9999->9999/tcp nervous_kepler
nita@infinity:~/work/infinite-money > http :9999/divide?a=1&b=0
HTTP/1.1 200
Connection: keep-alive
Content-Length: 54
Content-Type: text/plain;charset=UTF-8
Date: Tue, 10 Mar 2026 15:31:56 GMT
Keep-Alive: timeout=60
Infinity. You divided by zero. Welcome to the company.
At her old job, Nita had used something called Peregrine. It was an in house platform built by some dude that had been there forever, "and it did all of this for me" she thought as she looked for her boss again. She wanted to ask what server she should be deploying this to. Her old job had used Amazon's Elastic Container Service, at home she used Nomad from Hashicorp and for something like this she'd normally just knock out a quick compose file.
The one and only question that she had asked when it was her turn during her interview was "Do you use k8s?". She couldn't recall ever having been so glad to hear the word "No" in her life. "Horrible technology", she thought to no one in particular, "abstractions are supposed to simplify, not obscure".
She opened up another terminal and typed nvim docker-compose.yml and got to work.
services:
infinity-service:
image: infinity-service:test
ports:
- "9999:9999"
restart: unless-stopped
"That should do", she thought, "lets run docker compose up and make sure it works", somehow managing to preformat her thoughts.
nita@infinity:~/work/infinite-money > docker compose up
~/w/infinite-money > ticket-001 *… > app > docker compose up Tue 10 Mar 2026 12:20:16 PM EDT
Attaching to infinity-service-1
infinity-service-1 |
infinity-service-1 | . ____ _ __ _ _
infinity-service-1 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
infinity-service-1 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
infinity-service-1 | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
infinity-service-1 | ' |____| .__|_| |_|_| |_\__, | / / / /
infinity-service-1 | =========|_|==============|___/=/_/_/_/
infinity-service-1 | :: Spring Boot :: (v3.2.3)
infinity-service-1 |
infinity-service-1 | 2026-03-10T16:20:19.965Z INFO 1 --- [ main] com.infinity.Application : Starting Application v0.0.1 using Java 17.0.18 with PID 1 (/app/app.jar started by root in /app)
infinity-service-1 | 2026-03-10T16:20:19.974Z INFO 1 --- [ main] com.infinity.Application : No active profile set, falling back to 1 default profile: "default"
infinity-service-1 | 2026-03-10T16:20:21.773Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 9999 (http)
infinity-service-1 | 2026-03-10T16:20:21.791Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
infinity-service-1 | 2026-03-10T16:20:21.791Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
infinity-service-1 | 2026-03-10T16:20:21.844Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
infinity-service-1 | 2026-03-10T16:20:21.846Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1715 ms
infinity-service-1 | 2026-03-10T16:20:22.518Z INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
infinity-service-1 | 2026-03-10T16:20:22.595Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 9999 (http) with context path ''
infinity-service-1 | 2026-03-10T16:20:22.619Z INFO 1 --- [ main] com.infinity.Application : Started Application in 3.321 seconds (process running for 3.988)
It was 11:30am and Nita had been sitting in her cube all morning, not a co-worker nor a hint of management in sight. She stopped to check what she'd accomplished so far:
The ticket from Alice had only requested that it be ready for tomorrow. Her boss had implied during her orientation that there was a platform that she was supposed to be engineering, but it so far was as absent as the rest of her team. She decided to do what she'd been taught during her last role, if she had a problem to solve, why not build a tool?
fired up nvim again, this time excited by the chance to create. "But first", she thought, "time for lunch". She quickly typed a note into her editor, before locking her laptop and grabbing her purse.
To be continued...