<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Oskar Höstlund — Learning</title>
    <link>https://www.hostlund.com/learning/</link>
    <description>Software engineer in Berlin. Core banking interfaces, FinTS, and Java systems, after earlier work in technology strategy.</description>
    <atom:link href="https://www.hostlund.com/rss.xml" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Fri, 13 Nov 2026 09:52:16 GMT</lastBuildDate>
    <item>
      <title>Kafka within OpenShift</title>
      <link>https://www.hostlund.com/learning/kafka-openshift/</link>
      <guid isPermaLink="true">https://www.hostlund.com/learning/kafka-openshift/</guid>
      <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[An outline of Apache Kafka — brokers, topics, replication, KRaft, clients, consumer groups, and Kafka Connect — and of running it on OpenShift with the Strimzi operator.]]></description>
      <content:encoded><![CDATA[<p>This is an outline of Apache Kafka as a system, and of running a cluster on OpenShift with the Strimzi operator. It is generic study notes. It is not a write-up of a cluster I operate, and it does not describe an employer's system.</p>
<p>The language notes next to this one are <a href="/learning/modern-java/">modern Java</a> and <a href="/learning/go-or-rust/">Go or Rust</a>. They are a separate thread.</p>
<p>Where a line says TODO, that part is still to be written from the docs, in my own words.</p>
<h2>Why this note exists</h2>
<p>I want to be able to explain the architecture, a consumer group, a connector, and the OpenShift custom resources without the docs open.</p>
<blockquote>
<p><strong>TODO:</strong> turn that into a short list of questions I can answer out loud when this note is finished.</p>
</blockquote>
<h2>Brokers, topics, and partitions</h2>
<p>A Kafka cluster is a set of brokers. Producers append records to topics. A topic is split into partitions, and each partition is an ordered log. A record in a partition has an offset that increases along that partition.</p>
<p>Ordering is per partition. Records on different partitions have no order relative to each other. A key chooses the partition, so records with the same key stay in one partition and keep their order. A record with no key is spread across partitions.</p>
<blockquote>
<p><strong>TODO:</strong> a sketch of one topic with three partitions, and which keys land together.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> write the ordering rule in one sentence I would actually say.</p>
</blockquote>
<h2>Replication</h2>
<p>Each partition has a leader and zero or more followers. The in-sync replicas are the followers that have caught up with the leader. A record is available to consumers once it has reached the high watermark, the offset through which every in-sync replica has the record.</p>
<p><code>acks</code> on the producer, <code>min.insync.replicas</code>, and whether an unclean leader election is allowed are the three settings that decide what happens when a broker is lost. They trade durability against availability.</p>
<blockquote>
<p><strong>TODO:</strong> write those three in my own words, each with the failure it accepts. Take the definitions from the Kafka documentation, and link the page.</p>
</blockquote>
<h2>KRaft</h2>
<p>Kafka can store its own cluster metadata with KRaft, a Raft quorum of controllers, instead of an external ZooKeeper ensemble. <a href="https://kafka.apache.org/blog/2025/03/18/apache-kafka-4.0.0-release-announcement/">Apache Kafka 4.0</a> is the first major release that only runs in KRaft mode. ZooKeeper mode was removed in that release. The 3.9 line is the last 3.x release, and the bridge used to migrate a ZooKeeper cluster before moving to 4.0.</p>
<blockquote>
<p><strong>TODO:</strong> from the upgrade notes, write what an operator still runs in KRaft mode, and the order of a migration off ZooKeeper. Link the page. Do not reconstruct the steps from this paragraph.</p>
</blockquote>
<h2>Producers</h2>
<p>A producer batches records, chooses a partition (by key, or across partitions when there is no key), and waits according to <code>acks</code> before treating the send as done.</p>
<p>The idempotent producer and transactions are the two features I want a paragraph on. The idempotent producer avoids duplicates from retries. Transactions let a producer write to several partitions and commit them as one unit, at a cost in latency and complexity.</p>
<blockquote>
<p><strong>TODO:</strong> those two paragraphs, after reading the producer docs, plus a sentence on when I would leave transactions off.</p>
</blockquote>
<h2>Consumers and consumer groups</h2>
<p>A consumer reads a partition from an offset. A consumer group divides the partitions of its subscribed topics across its members, so each partition is read by one member of the group at a time. When membership changes, the group rebalances and partitions move.</p>
<p>Offsets are how the group remembers its place. Committing an offset before the work is finished means a crash loses that work. Committing after the work means a crash can deliver the same records again. Consumers are expected to tolerate the duplicate.</p>
<blockquote>
<p><strong>TODO:</strong> <code>enable.auto.commit</code> versus committing after the work, in my own words, with the failure of committing too early.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> one paragraph on cooperative and eager rebalancing, after the consumer docs. Name the protocol version I read about, including the next-generation rebalance protocol if that is the current one, and link it.</p>
</blockquote>
<h2>Kafka Connect and connectors</h2>
<p>Kafka Connect is a separate runtime for moving data in and out of Kafka. A source connector writes into Kafka. A sink connector writes out of it. Connect workers can run as their own cluster, with a config topic, an offset topic, and a status topic.</p>
<p>A connector is the configured job. A task is a unit of that job, and a connector can have several tasks. A converter turns the record's payload into bytes or back, for example JSON or Avro. Those three are different things, and it is easy to talk about them as if they were one.</p>
<blockquote>
<p><strong>TODO:</strong> write connector, task, and converter in one sentence each, from the Connect docs.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> name one source and one sink I want to be able to configure, after I have picked them. Leave the names out until then.</p>
</blockquote>
<h2>Kafka on OpenShift</h2>
<p><a href="https://strimzi.io/">Strimzi</a> is a Kubernetes operator for Apache Kafka. On OpenShift the cluster is declared as custom resources, and the operator reconciles them into pods, services, and configuration. Red Hat's distribution of that operator was AMQ Streams, and has been renamed Streams for Apache Kafka. The rename is recorded in the <a href="https://docs.redhat.com/en/documentation/red_hat_streams_for_apache_kafka/2.9/html/release_notes_for_streams_for_apache_kafka_2.9_on_rhel/ref-name-change-str">Streams for Apache Kafka 2.9 release notes</a>. Older pages still say AMQ Streams. The custom resources are the Strimzi ones either way.</p>
<p>The resources this note should be able to tell apart:</p>
<ul>
<li><code>Kafka</code> is the cluster: brokers, and the controller quorum. KRaft or, on older versions, ZooKeeper is a setting of this resource.</li>
<li><code>KafkaTopic</code> is a topic, so topic configuration lives in the cluster's API rather than only in a <code>kafka-topics</code> command.</li>
<li><code>KafkaUser</code> is a user and its access control, when the user operator is enabled.</li>
<li><code>KafkaConnect</code> is a Connect cluster.</li>
<li><code>KafkaConnector</code> is a connector deployed onto that Connect cluster.</li>
</ul>
<p>The topic operator and the user operator are switched on from the <code>Kafka</code> resource. Without them, <code>KafkaTopic</code> and <code>KafkaUser</code> are not reconciled.</p>
<p>A connector can also be created by posting its configuration to the Connect REST API. <code>KafkaConnector</code> is the operator's way of holding that configuration as a resource. They end up as the same connector inside Connect. The difference is what owns the configuration.</p>
<blockquote>
<p><strong>TODO:</strong> from the current Strimzi docs, summarise a minimal <code>Kafka</code> resource and the listener types I would actually choose on OpenShift. Link the doc. Keep the summary short enough that I can redraw it.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> write how the topic operator and the user operator are enabled, from the docs, in a few lines.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> say which I would use, <code>KafkaConnector</code> or the REST API, and why, after reading both.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> storage, metrics, and what a rolling restart of brokers does to clients. Three short paragraphs, each from a doc page I link.</p>
</blockquote>
<h2>Open questions</h2>
<blockquote>
<p><strong>TODO:</strong> questions go here as they come up. None yet beyond the placeholders above.</p>
</blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Go on this site: the camt and pain readers</title>
      <link>https://www.hostlund.com/learning/go-on-this-site/</link>
      <guid isPermaLink="true">https://www.hostlund.com/learning/go-on-this-site/</guid>
      <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[How the two Go programs behind the camt.053 and pain.001 pages work: request path, packages, types, and the Go idioms, read from Java.]]></description>
      <content:encoded><![CDATA[<p>The <a href="/experience/">work page</a> points at the <a href="/software/">software page</a>. Two of the tools there read an ISO 20022 file in Go: a camt.053 bank statement, and a pain.001 credit transfer. This note is a reading of that code for someone coming from Java. It describes the programs in this repository. It is not a claim about a production payment system.</p>
<p>The decision overview for spending more time on Go or on Rust is <a href="/learning/go-or-rust/">a separate note</a>. This one is only the code that is already here.</p>
<p>The examples are two programs, not two files. Each program is a <code>package main</code> split across a handler, a parser, and a test.</p>
<h2>Where the code is</h2>
<p>The statement reader:</p>
<ul>
<li><code>netlify/functions/camt/main.go</code> is the entry point.</li>
<li><code>netlify/functions/camt/parse.go</code> turns camt.053 XML into the JSON the page shows.</li>
<li><code>netlify/functions/camt/parse_test.go</code> checks the parser against the example file.</li>
<li><code>netlify/functions/camt/go.mod</code> names the module <code>hostlund.com/camt</code> and depends on <code>github.com/aws/aws-lambda-go</code> v1.55.1. The <code>go</code> line says 1.26.</li>
</ul>
<p>The payment reader:</p>
<ul>
<li><code>netlify/functions/pain/main.go</code></li>
<li><code>netlify/functions/pain/parse.go</code></li>
<li><code>netlify/functions/pain/parse_test.go</code></li>
<li><code>netlify/functions/pain/go.mod</code>, module <code>hostlund.com/pain</code>, with the same dependency.</li>
</ul>
<p>The two programs do not import each other. The namespace wrapper and the small string helpers are copied into each <code>parse.go</code>.</p>
<p>The example documents the tests read are <code>static/samples/camt053-example.xml</code> and <code>static/samples/pain001-example.xml</code>. The software page describes both as written for the site: made-up names and amounts, and a specimen IBAN.</p>
<h2>What each program does</h2>
<p><code>parseCamt</code> in <code>netlify/functions/camt/parse.go</code> decodes a bank-to-customer statement. The document element it maps is <code>BkToCstmrStmt/Stmt</code>. For each statement it builds a <code>statementView</code>: an id, a creation time, the account (IBAN, or the other account id when there is no IBAN, plus currency and name), the balances, and the entries. Balance codes such as <code>OPBD</code> and <code>CLBD</code> become labels ("Opening booked", "Closing booked") in <code>balanceLabel</code>. Credit and debit indicators <code>CRDT</code> and <code>DBIT</code> become "Credit" and "Debit" in <code>direction</code>.</p>
<p>An entry can contain transaction details. <code>entryViews</code> flattens <code>NtryDtls/TxDtls</code>. When those details exist, the amount prefers the transaction amount, then the detail amount, then the entry amount. The counterparty depends on the direction: a debit's other party is the creditor, and a credit's other party is the debtor, falling back to the creditor when the debtor name is empty. That logic is <code>counterparty</code>. When an entry has no transaction details, the view still has the entry amount, dates, status, servicer reference, and additional info.</p>
<p>The group header of the sample statement (<code>GrpHdr</code>, message id <code>SAMPLE-2026-02</code>) is not mapped. <code>encoding/xml</code> skips elements that no struct field asks for.</p>
<p><code>parsePain</code> in <code>netlify/functions/pain/parse.go</code> decodes a customer credit transfer initiation, <code>CstmrCdtTrfInitn</code>. The JSON is one <code>paymentView</code>: the group header (message id, created time, initiator, count, control sum) and a list of payment blocks. Each block has the debtor, the debtor IBAN (or the other account id), the debtor BIC (<code>BICFI</code>, or the older <code>BIC</code> tag when <code>BICFI</code> is empty), the execution date, and the transfers. A transfer's account is the creditor IBAN, or the other account id. Remittance is the unstructured <code>Ustrd</code> lines joined with spaces.</p>
<p>A few parsed fields are not copied onto the JSON. On a transfer, <code>InstrId</code> and the creditor BIC are unmarshaled and then left off <code>transferView</code>. The page does not show them.</p>
<p>Both parsers reject an empty body and a document that does not have the expected shape. camt returns "no statement was found in the file" when no <code>Stmt</code> was mapped. pain returns "no payment was found in the file" when the message id is empty and there are no payment blocks.</p>
<h2>How the Experience section shows them</h2>
<p>The work page does not paste this source. The sentence there links to the software page. The software page links to <code>/camt/</code> and <code>/pain/</code>, and describes the two readers in a sentence each.</p>
<p><code>src/pages/camt.js</code> posts the XML to <code>/api/camt</code> and renders <code>statements</code>: the account line, a balance table (<code>label</code>, <code>date</code>, <code>creditDebit</code>, <code>amount</code>, <code>currency</code>), and an entry table (<code>bookingDate</code>, <code>valueDate</code>, <code>creditDebit</code>, <code>amount</code>, <code>counterparty</code>, <code>endToEndId</code> or <code>reference</code>, <code>remittance</code>).</p>
<p><code>src/pages/pain.js</code> posts to <code>/api/pain</code> and renders the payment object itself: initiator, message id, count, control sum, and each block's debtor, IBAN, BIC, execution date, and transfers.</p>
<p>Both pages also show the example XML and say the file is not stored. Nothing in the Go writes the body to disk or to a database. The response sets <code>cache-control: no-store</code>.</p>
<p>The third software page, FinTS, is not Go.</p>
<h2>How a request is handled</h2>
<p>The shape of <code>main.go</code> is the same in both programs.</p>
<p><code>handler</code> accepts only <code>POST</code>. Anything else is HTTP 405. <code>requestBody</code> base64-decodes the body when <code>IsBase64Encoded</code> is set, which is the API Gateway proxy behaviour, and otherwise uses the body as bytes. <code>evaluate</code> rejects a body over <code>maxBytes</code> (1000000) with HTTP 413, calls the parser, and on success returns 200. camt wraps the slice as <code>{"statements": ...}</code>. pain marshals the <code>paymentView</code> as the whole body. A parser error becomes HTTP 400 with <code>{"error": "..."}</code>.</p>
<p><code>main</code> has two modes. The statement program reads stdin when <code>CAMT_DEV</code> is <code>stdin</code>. The payment program does the same when <code>PAIN_DEV</code> is <code>stdin</code>. In that mode it prints the status code, a newline, and the JSON. Otherwise it calls <code>lambda.Start(handler)</code>.</p>
<p>In <code>gatsby develop</code>, <code>gatsby-node.js</code> mounts <code>POST /api/camt</code> and <code>POST /api/pain</code>. Each mount runs <code>go run .</code> in the function directory with that environment variable set to <code>stdin</code>, passes the request body as the process input, and splits stdout on the first newline into a status and a payload. The timeout is 60 seconds. If the process fails or prints no newline, the dev server responds 500 with <code>{"error":"The file could not be read."}</code>.</p>
<p>On the deployed site, <code>netlify.toml</code> redirects <code>/api/camt</code> to <code>/.netlify/functions/camt</code> and <code>/api/pain</code> to <code>/.netlify/functions/pain</code>. Nothing in this repo sets <code>CAMT_DEV</code> or <code>PAIN_DEV</code> for that deploy, so <code>main</code> takes the <code>lambda.Start</code> path. The Gatsby build command in <code>netlify.toml</code> is <code>npm install &#x26;&#x26; npm run build</code>. It does not compile the Go. Netlify builds a function from each of <code>netlify/functions/camt</code> and <code>netlify/functions/pain</code>, which are <code>package main</code> directories with their own <code>go.mod</code>.</p>
<h2>How to build and run them</h2>
<p>From <code>netlify/functions/camt</code> or <code>netlify/functions/pain</code>:</p>
<pre><code class="language-bash">go test
go run .
</code></pre>
<p><code>go test</code> runs <code>TestExampleStatement</code> or <code>TestExamplePayment</code>, which read the sample XML three directories up, and <code>TestRejectsEmptyAndOtherXML</code>, which checks that an empty body and an unrelated <code>&#x3C;Document></code> are errors.</p>
<p><code>go run .</code> with neither dev variable set starts the Lambda handler and waits for an invocation. The path that the site uses locally is the dev variable, through the Gatsby process above. Sending a file by hand looks like this, from the function directory:</p>
<pre><code class="language-bash">CAMT_DEV=stdin go run . &#x3C; ../../../static/samples/camt053-example.xml
</code></pre>
<p>The first line of stdout is the status code. The rest is the JSON.</p>
<h2>Types and functions</h2>
<p>There are no classes. The data types are structs. The XML structs carry <code>xml</code> struct tags, and the view structs carry <code>json</code> struct tags. A tag such as <code>xml:"BkToCstmrStmt>Stmt"</code> maps a nested element. <code>xml:"Ccy,attr"</code> maps an attribute. <code>xml:",chardata"</code> maps the text inside the element. A slice field maps repeated elements: <code>[]entry</code> with <code>xml:"Ntry"</code>, or <code>[]string</code> with <code>xml:"RmtInf>Ustrd"</code>.</p>
<p>In the Java libraries this is closest to JAXB or Jackson annotations on fields. The path syntax (<code>Parent>Child</code>) is doing the job a chain of nested types would do. Both styles are in this code. <code>amount</code> is its own struct because currency is an attribute and the value is character data. The account name is a string with a path tag, <code>xml:"Ownr>Nm"</code>, because nothing else from that subtree is needed.</p>
<p><code>localNames</code> is the interesting type. It wraps an <code>xml.Decoder</code> and implements <code>Token() (xml.Token, error)</code>. <code>parseCamt</code> and <code>parsePain</code> pass it to <code>xml.NewTokenDecoder</code>. On each start and end element, and on each attribute, the method clears <code>Name.Space</code>. The namespace URI is discarded, so one set of structs accepts a camt.053 whose <code>xmlns</code> is <code>urn:iso:std:iso:20022:tech:xsd:camt.053.001.02</code> and a later version of the same statement shape. The camt page says this in a sentence. The pain sample is <code>pain.001.001.09</code>, and the same wrapper is why the tag names matter and the URN does not.</p>
<p>The functions, in the order a request uses them:</p>
<p>camt: <code>main</code>, <code>handler</code>, <code>requestBody</code>, <code>evaluate</code>, <code>parseCamt</code>, <code>toView</code>, <code>entryViews</code>, <code>flatten</code>, <code>counterparty</code>, <code>direction</code>, <code>balanceLabel</code>, <code>firstText</code>, <code>cleanList</code>, <code>jsonStatus</code>, <code>jsonResponse</code>.</p>
<p>pain: <code>main</code>, <code>handler</code>, <code>requestBody</code>, <code>evaluate</code>, <code>parsePain</code>, <code>toView</code>, <code>firstText</code>, <code>cleanList</code>, <code>jsonStatus</code>, <code>jsonResponse</code>.</p>
<p><code>jsonResponse</code> in the camt program marshals on its own. In the pain program it calls <code>jsonStatus</code> and uses that result. The behaviour a caller sees is the same.</p>
<h2>Idioms, for a Java developer</h2>
<p><strong>One package, several files.</strong> Every file says <code>package main</code>. All of the functions and types in the directory are in one package, and <code>main</code> is the entry point, the same role as <code>public static void main</code>. There is no class wrapping them. Java would have made <code>parseCamt</code> a method on a class. Go lets the function sit at package scope.</p>
<p><strong>Visibility is the capital letter.</strong> <code>parseCamt</code>, <code>handler</code>, and <code>statement</code> start with a lowercase letter, so another module cannot call them. The tests are <code>package main</code>, not <code>package main_test</code>, so they can call <code>parseCamt</code> directly. A Java test in the same package can call a package-private method for the same reason. The module path in <code>go.mod</code> is the import path, closest to Maven coordinates, and nothing in this repo imports either module. They are programs, not libraries.</p>
<p><strong>Errors are values.</strong> <code>parseCamt</code> returns <code>([]statementView, error)</code>. The caller writes <code>if err != nil</code>. There is no <code>throw</code> and no checked exception. <code>fmt.Errorf</code> builds the error. HTTP status is not the Go <code>error</code> return. <code>handler</code> almost always returns a nil error and puts 400, 405, or 413 on the response. The <code>error</code> return is the function runtime's failure channel. A nil error with a 400 response means "the invocation succeeded, the file did not."</p>
<p><strong>Structs instead of beans.</strong> A struct is a block of fields. The zero value is usable: pain returns <code>paymentView{}</code> on failure, which is the analogue of an empty object whose fields are <code>""</code>, <code>nil</code>, or <code>0</code>. There is no constructor unless someone writes one. These files do not. <code>toView</code> fills the struct in a composite literal, which reads like a constructor call with named arguments.</p>
<p><strong>Slices instead of ArrayList.</strong> <code>views := make([]statementView, 0, len(doc.Statements))</code> allocates a slice with length 0 and enough capacity for the statements, then <code>append</code> adds to it. The capacity argument is the same idea as <code>new ArrayList&#x3C;>(n)</code>. <code>append</code> may allocate a larger array if the capacity is exceeded. <code>flatten</code> starts from a nil slice and appends. A nil slice marshals as JSON <code>null</code> if it is ever returned directly. The views in this code are built with <code>make</code>, so an empty list of balances or transfers marshals as <code>[]</code>.</p>
<p><strong>The interface is implicit.</strong> <code>localNames</code> does not declare that it implements a type. <code>xml.NewTokenDecoder</code> asks for a <code>TokenReader</code>, which is an interface with one method, <code>Token() (Token, error)</code>. Having the method is enough. Java would say <code>implements TokenReader</code>. The method has a value receiver, <code>func (l localNames) Token()</code>, and the decoder pointer lives inside the struct, so the copy still reads the same underlying decoder. The loop <code>for i := range element.Attr</code> uses the index because a <code>range</code> value is a copy. Writing through the index updates the attribute that is returned. That is a common Go slip, and this code avoids it.</p>
<p><strong><code>switch</code> does not fall through.</strong> <code>direction</code> and <code>balanceLabel</code> switch on the upper-cased code and return from each case. There is no <code>break</code>. A case ends at the next case.</p>
<p><strong>Variadic helpers.</strong> <code>firstText(values ...string)</code> is <code>String...</code> in Java. It returns the first value that is not blank after <code>strings.TrimSpace</code>. Dates use it so a date-only tag wins when it is present and a date-time tag is the fallback: <code>firstText(item.BookingDate, item.BookingTime)</code>.</p>
<p><strong><code>any</code>.</strong> <code>map[string]any{"statements": statements}</code> is a map from string to an arbitrary value, closest to <code>Map&#x3C;String, Object></code>. <code>any</code> is a predeclared alias for <code>interface{}</code>. This code does not use type parameters beyond that.</p>
<p><strong>Short declarations.</strong> <code>stmt := statements[0]</code> declares and assigns. The type comes from the right-hand side, the same information <code>var</code> carries in Java, written at the call.</p>
<p><strong>Tests.</strong> <code>parse_test.go</code> imports the <code>testing</code> package. <code>t.Fatal</code> fails the test and stops it, in the role of an assertion failure. There is no test class and no framework in these files. <code>go test</code> finds the files by the <code>_test.go</code> suffix.</p>
<h2>What this code does not use</h2>
<p>There are no goroutines, no channels, and no mutexes. Concurrency is not a reason this program is in Go. The reasons visible in the code are the small runtime, the XML and JSON libraries, a single binary, and the function entry point.</p>
<p>There is no shared internal package. The duplication of <code>localNames</code>, <code>amount</code>, <code>firstText</code>, and <code>cleanList</code> is the cost of that.</p>
<blockquote>
<p><strong>TODO:</strong> after changing one of the duplicated helpers, write whether I still want two copies. That is my note to make once I have edited the code, not a refactor hidden in this write-up.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> a line on what felt familiar from Java the first time I reread <code>parse.go</code>, and what did not. I want that in my own words.</p>
</blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Go or Rust: deciding which to commit to</title>
      <link>https://www.hostlund.com/learning/go-or-rust/</link>
      <guid isPermaLink="true">https://www.hostlund.com/learning/go-or-rust/</guid>
      <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[A decision overview for a Java and Spring Boot engineer in banking: learning curve, concurrency, memory, tooling, domains, Kafka, and job-market signals. The conclusion is left open.]]></description>
      <content:encoded><![CDATA[<p>This note is a decision overview for picking a second language next to Java. The Java side of the comparison is the outline in <a href="/learning/modern-java/">what modern Java looks like</a>. The work it is written against is the work on the <a href="/experience/">work page</a>: Java services, Jakarta EE, Spring Boot, payments, and bank interfaces. Nothing here is a record of having shipped Go or Rust in production. The Go that already runs on this site is a separate reading note, <a href="/learning/go-on-this-site/">the camt and pain readers</a>.</p>
<p>The conclusion is blank on purpose. The last section is a checklist, and the lines marked TODO are where my own decision goes once I have done the looking.</p>
<h2>Learning curve, coming from Java</h2>
<p>Go is a small language with a garbage collector. A Java developer already knows the shape of it: packages, functions, structs, interfaces, loops, and values passed around. The surprises are local. Names that start with a capital letter are public. Errors are returned, not thrown. There is no inheritance of implementation. Interfaces are satisfied by having the methods, with no <code>implements</code> clause. The compiler is fast, and <code>gofmt</code> leaves one layout.</p>
<p>Rust is a different kind of step. Ownership, borrowing, and lifetimes are the language, not a library. The compiler rejects programs a Java compiler would accept, because the Java program would rely on the garbage collector and on the programmer to avoid use-after-free and data races. Reading Rust gets easier before writing it does. Writing it well is the long part.</p>
<blockquote>
<p><strong>TODO:</strong> after a short exercise in each, write which of those surprises actually slowed me down. Keep it to what I hit, not to a general claim about either language.</p>
</blockquote>
<h2>Concurrency</h2>
<p>Go's unit of concurrency is the goroutine: a function the runtime schedules onto operating-system threads. Goroutines are cheap enough that a server can use one per request and write blocking code. Channels move values between them. <code>select</code> waits on more than one. Sharing memory with a mutex is also ordinary Go. The feel is close to the pitch of Java virtual threads — blocking code, many threads — with channels as an extra primitive. The comparison with virtual threads belongs next to the <a href="/learning/modern-java/">Java note</a> once that section is filled in.</p>
<p>Rust has threads and mutexes too. The type system tracks <code>Send</code> and <code>Sync</code>, so sharing across threads is a type error when it is unsafe. The usual way to write a network service is <code>async</code>/<code>await</code> on a runtime such as Tokio. That is a different model from goroutines. Suspension points are marked in the code, and the types of the futures can dominate the signature. There is no garbage collector pausing the process, and there is also no runtime that makes a blocking call inside an async function cheap or correct.</p>
<blockquote>
<p><strong>TODO:</strong> one paragraph in my own words on which model I can hold in my head while reading someone else's code. Goroutines and channels, or async Rust. Write it after reading a real example of each, and link the example.</p>
</blockquote>
<h2>Memory</h2>
<p>Go is garbage collected. The compiler decides whether a value stays on the stack or escapes to the heap. Pointers exist. The programmer does not free memory.</p>
<p>Rust gives each value an owner. A borrow is a reference with a lifetime, and the compiler checks that the reference does not outlive the value. There is no garbage collector. The cost shows up in the types (<code>String</code> versus <code>&#x26;str</code>, <code>Vec</code> versus a slice, <code>Box</code>, <code>Rc</code>, <code>Arc</code>) rather than in a collector.</p>
<p>For service code that spends its time on I/O and JSON, the collector is rarely the thing a Java developer is trying to escape. For a parser or a component on a latency budget, the collector is the thing Rust is for.</p>
<blockquote>
<p><strong>TODO:</strong> say which of those two sentences matches the work I want to be hired for. Leave it empty until the checklist below has an answer.</p>
</blockquote>
<h2>Tooling and ecosystem</h2>
<p>Go ships as the <code>go</code> command: modules, <code>go test</code>, <code>go fmt</code>, and a standard library that covers HTTP, JSON, and concurrency. A program builds to one static binary. The <a href="/learning/go-on-this-site/">camt and pain readers</a> on this site are that shape: a <code>go.mod</code>, the standard <code>encoding/xml</code> and <code>encoding/json</code> packages, and one third-party module for the Netlify function signature.</p>
<p>Rust ships as rustup and Cargo. Dependencies come from crates.io. <code>rustfmt</code> and Clippy are the usual format and lint tools. Compiler errors are specific. Compile times are longer than Go's, and the ecosystem is large, with more choice of crates for the same job.</p>
<blockquote>
<p><strong>TODO:</strong> note the first library I had to go looking for in each language, and whether the standard library would have done it.</p>
</blockquote>
<h2>Where the languages turn up</h2>
<p>These are the domains I am weighing, in the terms of the work rather than as a ranking.</p>
<p>Cloud-native and Kubernetes tooling, including OpenShift, is largely Go. Kubernetes itself, the Docker engine, containerd, Terraform, and Prometheus are Go codebases. A controller written with Kubebuilder is Go. OpenShift is a Kubernetes distribution, so the same applies to a lot of the platform tooling around it. The Strimzi operator, which the <a href="/learning/kafka-openshift/">Kafka note</a> is about, is a Java project (<a href="https://github.com/strimzi/strimzi-kafka-operator">strimzi-kafka-operator</a>). Reading that operator is Java work.</p>
<p>Microservices in the style of a Spring Boot service — HTTP, JSON, a database or a queue — are a short step in Go: one binary, a garbage collector, blocking handlers or goroutines. The same service can be written in Rust. The distance from Spring is larger, because the memory model and the async runtime come along with the HTTP stack.</p>
<p>CLIs are a natural Go program, for the same reason: a static binary and a fast edit-compile cycle. Rust is also a common CLI language. The difference I care about at the start is how long it takes to get the first useful version, not the ceiling.</p>
<p>Systems and performance-critical code is Rust's case. The language is built to express ownership of memory and to exclude data races. Go runs plenty of demanding infrastructure and accepts garbage collection as the trade. I want the later comparison with virtual threads to stay honest about that trade, once the Java note has the Loom section filled in.</p>
<p>WebAssembly is a first-class Rust target, used for code that runs in the browser and in some edge runtimes. Go can compile to WebAssembly. The output includes the Go runtime, so a small module is a less usual reason to pick Go.</p>
<blockquote>
<p><strong>TODO:</strong> mark which of these domains I actually want the second language for. One of them, not all of them.</p>
</blockquote>
<h2>Kafka, streaming, and fintech</h2>
<p>The Java client that ships with Apache Kafka is the client a Spring service already has a path to. Go has clients in wide use, among them <a href="https://github.com/segmentio/kafka-go">kafka-go</a> and <a href="https://github.com/confluentinc/confluent-kafka-go">confluent-kafka-go</a>. Rust has <a href="https://github.com/fede1024/rust-rdkafka">rdkafka</a>, which wraps librdkafka. This note does not pick one.</p>
<p>On the banking side, the work described on this site is Java: interfaces, FinTS, file and batch integration, Camunda in an earlier role. Neither Go nor Rust replaces that for the application itself. Go is already how this site reads a camt.053 statement and a pain.001 payment: a small program, XML in, JSON out, deployed as a function. That is a payments-shaped task of the kind Go is comfortable for, and a task a Rust parser would also suit. It is one data point about a fit, and it is <a href="/learning/go-on-this-site/">written up on its own</a>.</p>
<blockquote>
<p><strong>TODO:</strong> if a streaming example is the practice project, write down which client I used and what broke. Do not turn this section into a claim that one language is the fintech language.</p>
</blockquote>
<h2>Job-market signals</h2>
<p>Qualitatively, from the shape of the work rather than from a survey table:</p>
<p>Java is still the language the core-banking and payments application roles are written in. A second language adds listings. It does not retire the first one.</p>
<p>Go is the second language that shows up beside Kubernetes, platform, and cloud-native service roles. That follows from where the tooling is written, above, more than from a hiring statistic.</p>
<p>Rust shows up on a narrower set of roles: systems, security-sensitive components, and infrastructure where the memory model is the requirement. Fewer banking application posts ask for it. The posts that do are more specific.</p>
<p>The <a href="https://survey.stackoverflow.co/">Stack Overflow Developer Survey</a> publishes language rankings each year. This note deliberately does not copy a percentage, a salary figure, or a "most loved" rank out of it. Those numbers go stale, and a pasted figure with no table under it is worse than none. Read the current edition, and read current listings.</p>
<blockquote>
<p><strong>TODO:</strong> look at current listings for the kind of role I would actually take in Berlin. Write what the posts asked for, with links to the posts. If a number ends up in this note, it comes from one of those pages and the link stays next to it.</p>
</blockquote>
<h2>A checklist, and then my conclusion</h2>
<p>Answer these in order. The decision is the last line, and it is empty until then.</p>
<ol>
<li>Name the work the second language is for: platform and Kubernetes tooling, service backends beside Java, or systems and performance-critical code. One primary.</li>
<li>Do the small version of the program this site already has: read a file, return JSON. Once in Go, once in Rust. Write down which one I was willing to finish, and where I got stuck. The Go version can start from <a href="/learning/go-on-this-site/">the readers already in this repo</a>.</li>
<li>Read one codebase I did not write. A Kubernetes controller in Go, and a small Rust program of similar length. Note which reading was usable on a weekday evening.</li>
<li>Read current Berlin listings and the current Stack Overflow survey. Link them. Count nothing in this note that I cannot point at.</li>
<li>Decide whether the Kafka-on-OpenShift study needs either language. The Strimzi operator is Java, and the first Kafka client in a Spring service is the Java client. A second language can still be the right use of the time. It is a separate question from finishing that note.</li>
<li>Write the conclusion in one paragraph: which language I will commit practice time to, for how long, and what I will ship with it. The other language stays available. This note is about the commitment, not about a ban.</li>
</ol>
<blockquote>
<p><strong>TODO:</strong> my conclusion, after the checklist. One paragraph. Leave the paragraph out rather than guess it.</p>
</blockquote>
<p>Read as a checklist with the conclusion still empty, Go is the shorter step from Java into Kubernetes-side and service work, and it is the language already running on this site. Rust is the longer step, and the one that matters if the target work is ownership and the absence of a garbage collector. Neither replaces Java for the core banking work on the work page. That leaning is a summary of the sections above. It is not the decision in item 6.</p>]]></content:encoded>
    </item>
    <item>
      <title>Languages: what modern Java looks like</title>
      <link>https://www.hostlund.com/learning/modern-java/</link>
      <guid isPermaLink="true">https://www.hostlund.com/learning/modern-java/</guid>
      <pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[An outline of modern Java — records, sealed types, pattern matching, text blocks, var, virtual threads, modules, and streams — from a Jakarta EE and Spring Boot background. The start of a series that will compare Java with Go and Rust.]]></description>
      <content:encoded><![CDATA[<p>This is an outline, not a finished note. The background it starts from is the Java on the <a href="/experience/">work page</a>: older Jakarta EE, and Spring Boot. The point of writing it down is to have a clear picture of the language as it is now, before later notes set the same ideas next to Go and Rust.</p>
<p>Where a line says TODO, that part is still to be written from my own reading and from code I actually have in front of me.</p>
<h2>Why this note exists</h2>
<p>The language moved, in public, through a series of JEPs. A lot of service code still looks like Java 8: classes with getters, loops, and a classpath of jars. This note is the list of changes I want to be able to explain without looking them up.</p>
<blockquote>
<p><strong>TODO:</strong> write down the language level and the frameworks in the code I actually work in, from the build files, rather than from memory.</p>
</blockquote>
<h2>Records</h2>
<p>A record (JEP 395, Java 16) is a class whose state is the list of components in the header. The compiler writes the constructor, the accessors, <code>equals</code>, <code>hashCode</code>, and <code>toString</code>.</p>
<blockquote>
<p><strong>TODO:</strong> a before and after against a small immutable type I would otherwise have written by hand in a JEE or Spring service.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> where a record is the wrong tool. Mutable state, and a type that needs a superclass, are the cases I want a sentence on.</p>
</blockquote>
<h2>Sealed types</h2>
<p>A sealed class or interface (JEP 409, Java 17) names the types that are allowed to extend or implement it. The set of subtypes is closed.</p>
<blockquote>
<p><strong>TODO:</strong> a small hierarchy, a result or a message, with the permitted subtypes written out.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> leave a pointer here for the later comparison with a Rust enum and with the usual Go approach of an interface plus unexported implementations. That comparison belongs in a later note. It is not written yet.</p>
</blockquote>
<h2>Pattern matching</h2>
<p>Pattern matching for <code>instanceof</code> (JEP 394, Java 16) binds the tested value, so the cast after the test goes away. Pattern matching for <code>switch</code> (JEP 441, Java 21) lets a switch cover types.</p>
<blockquote>
<p><strong>TODO:</strong> one example that switches on a sealed type, and what the compiler reports when a permitted subtype is missing from the switch.</p>
</blockquote>
<h2>Text blocks and var</h2>
<p>A text block (JEP 378, Java 15) is a multiline string literal. <code>var</code> (JEP 286, Java 10) infers the type of a local variable from its initializer. Neither one changes the type system. They change how a method reads.</p>
<blockquote>
<p><strong>TODO:</strong> paste a SQL or JSON literal I would otherwise have built with concatenation, and mark one <code>var</code> I would keep and one I would write out in full.</p>
</blockquote>
<h2>Virtual threads</h2>
<p>Virtual threads (JEP 444, Java 21, Project Loom) are threads scheduled by the JVM onto a smaller set of carrier platform threads. The style they are for is blocking I/O, with a thread per request, at a cost closer to an asynchronous runtime than to the old thread pool.</p>
<blockquote>
<p><strong>TODO:</strong> after reading the JEP, write what changes for a Spring Boot or servlet service that blocks on I/O, and what stays the same. Pin the notes on <code>synchronized</code>, thread locals, and pooling to the JEP, not to a summary from memory.</p>
</blockquote>
<blockquote>
<p><strong>TODO:</strong> this is the comparison point for goroutines. Leave the comparison until the Go note exists. The decision overview is <a href="/learning/go-or-rust/">Go or Rust</a>.</p>
</blockquote>
<h2>The module system</h2>
<p>The Java Platform Module System (JEP 261, Java 9) adds modules with <code>requires</code> and <code>exports</code>. A lot of Spring Boot and older JEE code still ships as a classpath of jars. "Module" in a Maven build is a different thing from a JPMS module.</p>
<blockquote>
<p><strong>TODO:</strong> state whether any project I touch builds with <code>module-info.java</code>. If the only modules in sight are Maven modules, say that in one sentence.</p>
</blockquote>
<h2>Streams</h2>
<p>The Stream API arrived in Java 8. It is the oldest item on this list, and the one most likely to already be in a codebase that otherwise looks like Java 8. A stream is a pipeline over a source: intermediate operations, then a terminal operation.</p>
<blockquote>
<p><strong>TODO:</strong> one loop I would rewrite as a pipeline, and one loop I would leave as a loop, taken from code I can point at.</p>
</blockquote>
<h2>What comes next</h2>
<p>The next note in this series is the decision overview, <a href="/learning/go-or-rust/">Go or Rust: deciding which to commit to</a>. It is about which language to spend the time on. It is not yet the feature-by-feature comparison.</p>
<p>Kafka on OpenShift is a separate note, <a href="/learning/kafka-openshift/">Kafka within OpenShift</a>.</p>
<blockquote>
<p><strong>TODO:</strong> a later note that takes records, sealed types, and virtual threads and sets them next to Go and Rust, once I can write the other side from the language rather than from a glossary.</p>
</blockquote>]]></content:encoded>
    </item>
  </channel>
</rss>
