Behind the Mystery of “Wrong” Qubit Ordering in Qiskit

Almost everyone starts learning quantum computing using single quantum systems (i.e. with just one qubit). However, soon enough we switch to multiple quantum systems (with two or more qubits) in our pursuit of knowledge about gates and algorithms. One of the first things we learn then are two-qubit gates, like the collection of controlled gates (ex. CNOT) or the SWAP gate.

This is usually the moment when you may get confused with Qiskit’s behaviour. You take a two-qubit gate like the SWAP gate, you apply it to |01⟩ for example, and in the results you see that nothing was swapped. In reality the gate worked as expected, and the confusion is rooted in the way Qiskit stores its state in memory. 

Why is this happening? Let us start with implementing a program that can demonstrate this oddity. 

Swapping Qubits’ States

There are many ways of changing the state of a two-qubit quantum system. I have already mentioned controlled gates which I believe are the most popular gates in quantum circuits nowadays. However, in this post I would like to use a simple example with a SWAP gate. This gate works in a very intuitive way, and is easy to understand even without robust quantum computing or computing in general knowledge. 

The SWAP gate works in a pretty straightforward way: it swaps the values of two qubits. How does it exactly work? Every quantum gate can be represented with a unitary matrix. What a unitary matrix is is beyond the scope of this post. If you are nonetheless interested, I can tell for starters that it is a complex square matrix that, when multiplied by its conjugate transpose, results in the identity matrix. You can read more about unitary matrices here

Without going into further details, here’s the matrix for the SWAP gate. 

{"id":"39","type":"$$","code":"$$SWAP\\,=\\begin{bmatrix}\n{1}&{0}&{0}&{0}\\\\\n{0}&{0}&{1}&{0}\\\\\n{0}&{1}&{0}&{0}\\\\\n{0}&{0}&{0}&{1}\\\\\n\\end{bmatrix}$$","backgroundColorModified":false,"backgroundColor":"#ffffff","font":{"family":"Arial","size":11,"color":"#000000"},"aid":null,"ts":1732694362039,"cs":"aRHqpC4Q3iTzJMzrMq9i9g==","size":{"width":182,"height":88}}

In order to apply it to a two qubits quantum system we have to multiply it by the matrix representing that system.

In short, two-qubit quantum systems are represented by a single column matrix that corresponds to every possible combination of those systems. You can see them all below.

{"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","aid":null,"code":"$$|00\\rangle =\\begin{bmatrix}\n{1}\\\\\n{0}\\\\\n{0}\\\\\n{0}\\\\\n\\end{bmatrix}\\,\\,\\,\\,|01\\rangle =\\begin{bmatrix}\n{0}\\\\\n{1}\\\\\n{0}\\\\\n{0}\\\\\n\\end{bmatrix}\\,\\,\\,\\,|10\\rangle =\\begin{bmatrix}\n{0}\\\\\n{0}\\\\\n{1}\\\\\n{0}\\\\\n\\end{bmatrix}\\,\\,\\,\\,|11\\rangle =\\begin{bmatrix}\n{0}\\\\\n{0}\\\\\n{0}\\\\\n{1}\\\\\n\\end{bmatrix}$$","id":"19","font":{"family":"Arial","color":"#000000","size":11},"ts":1732694341122,"cs":"glpliW/Ui8bRxvQjX7/Uig==","size":{"width":360,"height":88}}

(If you don’t know what these numbers are, please read my earlier blog post.)

When you apply this matrix to a ket, it simply swaps the second the the third coefficients. So in general:

{"backgroundColor":"#ffffff","backgroundColorModified":false,"id":"40","aid":null,"type":"$$","font":{"family":"Arial","size":11,"color":"#000000"},"code":"$$SWAP\\begin{bmatrix}\n{v_{1}}\\\\\n{v_{2}}\\\\\n{v_{3}}\\\\\n{v_{4}}\\\\\n\\end{bmatrix}=\\begin{bmatrix}\n{v_{1}}\\\\\n{v_{3}}\\\\\n{v_{2}}\\\\\n{v_{4}}\\\\\n\\end{bmatrix}$$","ts":1731396509048,"cs":"oQuinKOVInSv2hgis/c2KA==","size":{"width":152,"height":88}}

(If you do not know what kets are, you can read about them here.)

The SWAP Gate in Action 

In order to use the SWAP gate in our example, we must write a circuit that uses it. We start off with importing all necessary dependencies.

from itertools import product
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_aer import Aer

Then, we define a set of variables that we’ll use later in the program.

register_qubits = 2
register_bits = 2
zero_ket = [1, 0]
one_ket = [0, 1]
# Generate all possible combinations of zero_ket and one_ket
ket_combinations = list(product([zero_ket, one_ket], repeat=2))
first_qubit = 0
second_qubit = 1
shots = 100

Their purpose will come clear when they’re used later in the code.

As I mentioned before, we can have four different combinations of two basic computational states: |0⟩ and |1⟩. Just as a reminder, they’re |00⟩, |01⟩, |10⟩ and |11⟩. In order to fully demonstrate what the SWAP gate does, we have to take a look at all of those combinations. We created them on line 9 of this program:

ket_combinations = list(product([zero_ket, one_ket], repeat=2))

This line essentially combines |0⟩ and |1⟩ into a collection of |00⟩, |01⟩, |10⟩ and |11⟩. We can then iterate over these combinations.

for ket_combination in ket_combinations:

The actual circuit is created and run within the loop. I will not go into details here, but please read what quantum circuits are and quantum simulators are if you would like to know more.

    quantum_register = QuantumRegister(register_qubits, "q")
    classical_register = ClassicalRegister(register_bits, "c")
    circuit = QuantumCircuit(quantum_register, classical_register)
    circuit.initialize(ket_combination[0], first_qubit)
    circuit.initialize(ket_combination[1], second_qubit)
    circuit.swap(quantum_register[first_qubit], quantum_register[second_qubit])
    circuit.measure(quantum_register, classical_register)

    simulator = Aer.get_backend("qasm_simulator")
    transpiled_circuit = transpile(circuit, simulator)
    result = simulator.run(transpiled_circuit, shots=shots).result()
    print(f"For the first qubit {ket_combination[0]} and the second qubit {ket_combination[1]} the SWAP result is: {result.get_counts(circuit)}")

First off, we create a quantum and a classical register on lines 14 and 15. You can think of those registers as processor’s stack memory, the one that’s closest to it. The quantum register is used for storing qubits’ states, whereas the classical register is used for storing the values that are read out during measurement.

Then we implement the actual circuit (lines 16-20).

circuit = QuantumCircuit(quantum_register, classical_register)
circuit.initialize(ket_combination[0], first_qubit)
circuit.initialize(ket_combination[1], second_qubit)
circuit.swap(quantum_register[first_qubit], quantum_register[second_qubit])
circuit.measure(quantum_register, classical_register)

We define the circuit object by calling the QuantumCircuit(quantum_register, classical_register) constructor.

We then initialise our two qubits with whatever kets we have in a given loop iteration.

circuit.initialize(ket_combination[0], first_qubit)
circuit.initialize(ket_combination[1], second_qubit)

Finally, we apply the SWAP gate…

circuit.swap(quantum_register[first_qubit], quantum_register[second_qubit])

…and measure.

circuit.measure(quantum_register, classical_register)

If you plotted this circuit with the following line of code – circuit.draw(output=’mpl’) – you’d get this diagram.

(I’ll cover circuit diagrams in one of the future blog posts.)

It represent the following quantum operation: SWAP|00⟩.

Run the program, and you’ll get the following results.

For the first qubit [1, 0] and the second qubit [1, 0] the SWAP result is: {'00': 100}
For the first qubit [1, 0] and the second qubit [0, 1] the SWAP result is: {'01': 100}
For the first qubit [0, 1] and the second qubit [1, 0] the SWAP result is: {'10': 100}
For the first qubit [0, 1] and the second qubit [0, 1] the SWAP result is: {'11': 100}

The number 100 is the number of times a given circuit was run (it’s important in a non-one probability readout like in this example).

[1, 0] is a row matrix that can be also written as |0⟩, and [0, 1] is a row matrix that can be written as |1⟩. So reading the results as they are yields this:

  • |00⟩ → {’00’: 100}
  • |01⟩ → {’01’: 100}
  • |10⟩ → {’10’: 100}
  • |11⟩ → {’11’: 100}

It looks like this program doesn’t work at all! But it does. The confusion lies in the way Qiskit stores values internally. It uses little endian.

Little Endian

What is little endian, or endianness in general? Endianness is the order in which bytes within a word of digital data are transmitted over a data communication medium or addressed (by rising addresses) in computer memory, counting only byte significance compared to earliness. The name is taken from the novel Gulliver’s Travels. You can read more about it here.

It sure sounds complicated, but in simpler words, endianness determines from which end of memory space the bytes are ordered. There are two types of endianness: the big endian and the little endian.

  • A big-endian system stores the most significant byte of a word at the smallest memory address and the least significant byte at the largest.
  • A little-endian system, in contrast, stores the least-significant byte at the smallest address.

Big endian is thus closer to the way the digits of numbers are written left-to-right in English, comparing digits to bytes. Little endian, on the other hand, reverses the natural ordering of the bits.

Here is a visual representation of these two systems.

Source: https://en.wikipedia.org/wiki/Endianness#/media/File:32bit-Endianess.svg 

In the above example you can clearly see that while the big-endian preserves the natural ordering of a given processor word (or any binary value in general), the little-endian essentially reverses it. And this is the case with Qiskit. It uses internally little-endian to store quantum processing results, and hence prints them accordingly. For two-qubit systems it maps to the following list.

QubitsQiskit
|00⟩00
|01⟩10
|10⟩01
|11⟩11

Getting the Big-endian Result

It is possible in Qiskit to change the ordering of the qubits from little-endian to big-endian. However, it is quite limited. For example, it can be easily done for circuit drawings with the reverse_bits() function. Unfortunately, it looks like switching to big-endian for the Result object that we use to query for whatever we computed is not supported.

Conclusion

Interpreting results obtained from quantum circuits in Qiskit can be challenging. This is due to the fact that Qiskit uses little-endian to store computation results. It stores the least significant bit (the right-hand most) in the first memory index, and the rest of the bits follow. The result is hence a reverse of natural ordering that we expect in ket representations of quantum systems and can be confusing for new and experienced users.

The Standard Computational Bases

In the previous article, I covered the Bloch sphere, one of the most helpful visual tools for understanding qubits. The Bloch sphere allows us to represent a qubit’s state as a point in three-dimensional space. While this point represents the qubit’s state, it’s often even more useful to consider its position in relation to one of the three primary axes on the sphere – x, y, and z. These axes are not arbitrary; each one is linked to a unique set of orthonormal basis vectors, commonly referred to as computational bases.

In quantum computing, a computational basis provides a standardised framework to measure or affect a qubit’s state. If you’re unfamiliar with the term “orthonormal basis vectors,” consider reviewing this helpful Wikipedia page on orthonormality. These bases are “computational” because they lay the groundwork for every computation we perform in quantum computing.

While any set of orthonormal basis vectors could theoretically serve as computational bases in quantum computing, there are three primary ones that are used most frequently: the computational basis, the Hadamard basis, and the circular basis. Each of these serves a distinct purpose in interpreting and manipulating qubit states.

The Computational Basis

The computational basis, aligned with the z-axis of the Bloch sphere, is the foundation of quantum measurement. It’s the most common basis for reading the results of quantum computations because it directly corresponds to binary classical states, making it easier to translate quantum states into familiar classical outputs. The computational basis consists of two column vectors {"aid":null,"id":"1-0","backgroundColorModified":false,"type":"$$","code":"$$\\begin{bmatrix}\n{1}\\\\\n{0}\\\\\n\\end{bmatrix}$$","backgroundColor":"#ffffff","font":{"size":11,"color":"#000000","family":"Arial"},"ts":1730705283029,"cs":"htX4ZvSp/vZU5LxOdr4lKg==","size":{"width":17,"height":40}} and {"id":"1","font":{"color":"#000000","family":"Arial","size":11},"backgroundColorModified":false,"backgroundColor":"#ffffff","aid":null,"type":"$$","code":"$$\\begin{bmatrix}\n{0}\\\\\n{1}\\\\\n\\end{bmatrix}$$","ts":1730705302677,"cs":"KT/yysX+vZcUfh8RBD5pbQ==","size":{"width":17,"height":40}}, often written in Dirac notation as |0⟩ and |1⟩. These two states represent the “north” and “south” poles of the Bloch sphere, and they correspond to the conventional binary states, 0 and 1. When a qubit is measured in this basis, it “collapses” to one of these two poles, giving us a straightforward binary result.

The Hadamard Basis

Next, the Hadamard basis is associated with the x-axis of the Bloch sphere, which represents a state along the sphere’s equator rather than at its poles. This basis is one of the most powerful tools in quantum computing because it allows us to use any real number as a qubit’s state, not just the discrete values of 0 or 1. For a deeper dive into why this is significant, check out my article on quantum vs classical computing. The Hadamard basis enables superdense coding, a technique that leverages quantum superposition to represent classical bits in more complex ways. 

The Hadamard basis consists of two vectors {"backgroundColor":"#ffffff","id":"4-0","code":"$$\\begin{bmatrix}\n{\\frac{{\\sqrt[]{3}}}{2}}\\\\\n{\\frac{1}{2}}\\\\\n\\end{bmatrix}$$","aid":null,"type":"$$","font":{"family":"Arial","color":"#000000","size":11},"backgroundColorModified":false,"ts":1730791999404,"cs":"hTWAQA6GbNSUcVtenr2D2A==","size":{"width":34,"height":52}} and {"code":"$$\\begin{bmatrix}\n{\\frac{1}{2}}\\\\\n{-\\frac{{\\sqrt[]{3}}}{2}}\\\\\n\\end{bmatrix}$$","backgroundColor":"#ffffff","type":"$$","font":{"size":11,"color":"#000000","family":"Arial"},"aid":null,"backgroundColorModified":false,"id":"4-1","ts":1730792031290,"cs":"/1LehwHPaq45D7XFsEbg1g==","size":{"width":48,"height":52}}, denoted in Dirac notation as |+⟩ and |-⟩. These states lie along the equator of the Bloch sphere, representing the ability of qubits to exist in a superposition of 0 and 1 simultaneously. This property underlies many of quantum computing’s most promising applications, such as quantum parallelism, where the system explores multiple possibilities at once, and quantum encryption, where superpositions contribute to secure data encoding.

The Circular Basis

Finally, the circular basis aligns with the y-dimension of the Bloch sphere and introduces an essential concept called phase. In quantum mechanics, a qubit’s phase describes the relative angle or orientation of its wave function in its quantum state. This phase does not impact the probability of measuring the qubit in a particular state; rather, it influences how qubits interact with each other, particularly in superpositions and entangled states. 

This phase-based interference is foundational for many quantum algorithms and gate operations, such as the Hadamard gate and controlled-phase gates, which rely on phase manipulation to perform complex calculations. The circular basis uses two vectors {"backgroundColorModified":false,"backgroundColor":"#ffffff","font":{"color":"#000000","family":"Arial","size":11},"type":"$$","id":"5","code":"$$\\begin{bmatrix}\n{\\frac{1}{{\\sqrt[]{2}}}}\\\\\n{\\frac{1}{{\\sqrt[]{2}}}}\\\\\n\\end{bmatrix}$$","aid":null,"ts":1730792529020,"cs":"7EktR3+oTgrPSWxbYh2kZg==","size":{"width":36,"height":56}} and {"code":"$$\\begin{bmatrix}\n{\\frac{1}{{\\sqrt[]{2}}}}\\\\\n{-\\frac{1}{{\\sqrt[]{2}}}}\\\\\n\\end{bmatrix}$$","type":"$$","backgroundColor":"#ffffff","font":{"color":"#000000","family":"Arial","size":11},"id":"5","backgroundColorModified":false,"aid":null,"ts":1730792547294,"cs":"uBfxjoFz/ylEchizDQptRw==","size":{"width":48,"height":56}}, usually written as |i⟩ and |-i⟩ in Dirac notation, to describe these states. This notation represents the relative phase of the qubit, allowing for fine-tuned control over quantum states and facilitating advanced computational processes like error correction and phase estimation.

Conclusion

Understanding the computational bases—computational, Hadamard, and circular—is essential for anyone delving into quantum computing. These bases provide different frameworks for interpreting and manipulating qubit states, whether through straightforward binary measurement, superposition, or phase manipulation.

Each basis has a unique role: the computational basis is the go-to for classical readouts, the Hadamard basis introduces superpositions that allow for more complex computations, and the circular basis enables control over phase, crucial for advanced algorithms and gate operations.

By mastering these foundations, we gain a toolkit to understand and perform quantum operations more effectively, unlocking the full potential of qubits for quantum computing tasks. Whether you’re analysing measurement outcomes or engineering interference patterns, these computational bases form the core of your journey through quantum mechanics and computation.

Understanding the Bloch Sphere: A Visualisation of Qubits in Quantum Computing

In my previous blog post I briefly described the fundamental advantages of quantum computing, such as superposition and entanglement. The key player behind these powerful features is the qubit, which serves as the fundamental unit of quantum information. Unlike classical bits that represent only 0s and 1s, qubits can exist in multiple states at once due to the principles of superposition and entanglement. This allows quantum computers to solve certain problems much faster than classical computers.

But how do we actually represent a qubit? One of the most useful ways to visualise qubits is through the Bloch sphere, which is what I will explain in this post. The Bloch sphere gives us a geometrical representation of a qubit’s state, helping us better understand its behaviour and how it can be manipulated during quantum computations.

Qubit States on the Bloch Sphere

The Bloch sphere is a geometrical representation of a qubit’s state space. Named after physicist Felix Bloch, who contributed to nuclear magnetic resonance (NMR) theory, and was awarded a Nobel prize for this work, it is essentially a unit sphere in a three-dimensional space. Each point on this sphere represents a possible state of a qubit, with the surface of the sphere encoding all possible superpositions of the quantum states {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} and {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}.

In the traditional representation of a qubit, its state can be written as:

{"font":{"color":"#000000","family":"Arial","size":11},"aid":null,"id":"1","type":"$$","code":"$$a\\lbrack0\\rangle \\,+\\,b\\lbrack1\\rangle $$","backgroundColor":"#ffffff","backgroundColorModified":false,"ts":1728628958459,"cs":"pzD7Y3hCvVkJmrkKQ+sJwg==","size":{"width":80,"height":16}}

where {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} and {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}} are the basic states (think of them as the quantum version of 0 and 1), and a and b are complex numbers that describe the probabilities of the qubit being in state {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} or {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}.

The coefficients a and b can be written using complex numbers, which introduce both magnitude and phase. But rather than get bogged down in complex numbers, we can think of the Bloch sphere as a way of mapping these values into three dimensions that correspond to specific aspects of the qubit’s state.

The Three Dimensions of the Bloch Sphere

The Bloch sphere has three key axes that correspond to different aspects of a qubit’s state:

Z-axis (Computational Basis)

This dimension is the simplest to understand and relates to the “classical” states of a qubit: {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} and {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}

  • If the qubit is at the north pole of the Bloch sphere, it represents {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}}.
  • If the qubit is at the south pole, it represents {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}.

This is why the z-axis is often referred to as the computational basis, since these states are the fundamental building blocks of classical computing.

X-axis (Superposition on the Equator)

This dimension captures superposition, where the qubit exists in a state between {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} and {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}. A qubit in superposition might lie on the equator of the Bloch sphere, representing a mix of both classical states.

For example, the states {"code":"$$\\lbrack+\\rangle $$","id":"2-1","backgroundColorModified":false,"aid":null,"font":{"color":"#000000","family":"Arial","size":10},"backgroundColor":"#ffffff","type":"$$","ts":1728633544861,"cs":"G8l4wtC/5G/z3f7Vve0pUw==","size":{"width":18,"height":14}} and {"type":"$$","font":{"size":10,"color":"#000000","family":"Arial"},"backgroundColorModified":false,"id":"2","code":"$$\\lbrack-\\rangle $$","backgroundColor":"#ffffff","aid":null,"ts":1728633575159,"cs":"ZivHJVPxJ6Z6UfnmbbDmcg==","size":{"width":18,"height":14}} lie on the equator. These states are key to many quantum algorithms because they describe qubits that are equally likely to be measured as {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} or {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}.

The x-axis helps us understand how qubits are manipulated during quantum computations, particularly when using gates like the Hadamard gate, which create superposition.

Y-axis (Phase)

The y-axis deals with the phase of a qubit. Phase is a bit tricky—it’s not something we can directly measure, but it plays a critical role in quantum computations. Phase helps differentiate states that might have the same probabilities of being {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} or {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}, but which behave differently when subjected to certain quantum gates.

For example, the qubit can have a positive or negative phase ({"type":"$$","code":"$$\\lbrack i\\rangle $$","backgroundColor":"#ffffff","backgroundColorModified":false,"id":"10","aid":null,"font":{"family":"Arial","color":"#000000","size":12},"ts":1728633607114,"cs":"TKiTByTL2a/pmLuqdBhKyg==","size":{"width":13,"height":17}} or {"backgroundColorModified":false,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"aid":null,"backgroundColor":"#ffffff","code":"$$\\lbrack -i\\rangle $$","id":"8","ts":1728633631336,"cs":"r9QBSnt+6me+TR5iRpxvCA==","size":{"width":28,"height":17}}), which describes how the qubit oscillates over time. This oscillation (or rotation on the sphere) affects how qubits interfere with each other during quantum operations.

Putting It All Together

At any given moment, a qubit’s state can be represented by a point on the surface of the Bloch sphere. This point is determined by two angles:

  • θ (theta): This angle tells us how far the state is from the north or south pole (i.e., the computational basis). A qubit at the poles is in a classical state, while a qubit on the equator is in a superposition.
  • φ (phi): This angle defines where on the equator the qubit lies, describing the phase of the superposition.

We can mathematically represent any qubit state ({"font":{"color":"#000000","size":12,"family":"Arial"},"backgroundColor":"#ffffff","code":"$$\\lbrack\\psi\\rangle $$","id":"12","backgroundColorModified":false,"type":"$$","aid":null,"ts":1728633682411,"cs":"O46rls3dSzxr1042dHPIEw==","size":{"width":20,"height":17}}) using these two angles:

{"type":"$$","backgroundColorModified":false,"backgroundColor":"#ffffff","id":"13","aid":null,"font":{"size":11,"family":"Arial","color":"#000000"},"code":"$$\\lbrack\\psi\\rangle \\,=\\,\\cos\\left(\\frac{\\theta}{2}\\right)\\lbrack0\\rangle +\\,\\sin\\left(\\frac{\\theta}{2}\\right)e^{\\varphi^{i}}\\lbrack1\\rangle $$","ts":1728630633800,"cs":"LVHfevq05WQXYay9pf45Pg==","size":{"width":264,"height":40}}

In this equation, θ and φ map the qubit’s state to a specific point on the Bloch sphere, where we use trigonometry to describe its position. And since we have come to the point where we are talking about trigonometry, we can finally plot the Bloch sphere.


This model gives us an intuitive way to visualise qubits and the operations we perform on them. For example:

  • Quantum gates can be thought of as rotations of the qubit on the Bloch sphere. When we apply a quantum gate, we essentially rotate the point that represents the qubit’s state.
  • Superposition is clearly seen when a qubit lies on the equator, halfway between {"id":"2-1","code":"$$\\lbrack0\\rangle $$","aid":null,"backgroundColorModified":false,"backgroundColor":"#ffffff","type":"$$","font":{"color":"#000000","family":"Arial","size":12},"ts":1728633312005,"cs":"54cxfABlUHryeVsbIcegtg==","size":{"width":16,"height":17}} and {"code":"$$\\lbrack1\\rangle $$","backgroundColorModified":false,"id":"3","aid":null,"type":"$$","font":{"size":12,"color":"#000000","family":"Arial"},"backgroundColor":"#ffffff","ts":1728633332948,"cs":"dfJUZHYoh6Y8oqfTT3WFjQ==","size":{"width":16,"height":17}}.
  • Phase changes are rotations around the z-axis, altering the angle φ but keeping the qubit’s probabilities the same.

Most importantly, the Bloch sphere shows us how quantum operations work in three dimensions, even though we might only measure the final state in one dimension (the z-axis). It helps explain why quantum algorithms can achieve speedups by harnessing the full power of quantum mechanics.

Conclusion

The Bloch sphere is an incredibly useful tool for visualising the state of a qubit and understanding how quantum operations work. By representing qubit states as points on a sphere, we can grasp key quantum properties like superposition and phase, which are crucial for quantum computations. As we continue exploring quantum computing, the Bloch sphere will serve as a fundamental concept for understanding how qubits behave and how quantum gates manipulate them.

In the next post, I’ll dive deeper into the computational basis and how it’s used in quantum algorithms. If you’re curious about the notation used to represent qubit states, check out this article on bra-ket notation until then!

Quantum vs Classical Computing: How Different Are They?

Computation is a core aspect of modern technology, driving innovations across science, engineering, entertainment, and communication. But what exactly is a computation, and how does it differ in classical and quantum contexts?

What is Computation?

At its most fundamental level, computation refers to using technology to perform tasks, solve problems, and manipulate information. Whether it’s simulating physical systems, developing intelligent algorithms, or managing vast data sets, computation is the bedrock of all digital advancements. Classical computing, which powers most of today’s digital systems, has long been the standard. However, quantum computing, a revolutionary paradigm, promises to redefine what’s possible in the field.

Classical vs. Quantum Computation

Classical Computing

Classical computing is rooted in binary logic. The smallest unit of information is a bit, which can exist in one of two possible states: 0 or 1. Every computation a classical computer performs is deterministic, meaning the result is always predictable based on the input. Operations are executed using logic gates, and bits can be copied, stored, and manipulated across various systems and memory registers without loss of information.

To illustrate, consider a light switch. A light switch can only be ON or OFF just like a bit can only be 1 or 0. Classical computers, no matter how complex, are simply massive collections of these switches, manipulating them to perform calculations. While highly effective for many applications, classical computers are limited by their binary nature.

Quantum Computing

Quantum computing, on the other hand, is a game changer because it relies on principles from quantum mechanics, which describe the behaviour of particles at extremely small scales. Instead of bits, quantum computers use qubits as the fundamental unit of information. Unlike classical bits, qubits can exist in an infinite number of states.

The state of a qubit can be visualised using a Bloch sphere. Imagine a globe, with 0 at the South Pole and 1 at the North Pole. While a classical bit is like a light switch, only toggling between ON (1) or OFF (0), a qubit can be any point on the surface of this globe. It can blend 0 and 1 in a continuum of possibilities, opening up a much larger space for computation.

Moreover, quantum operations exploit unique quantum phenomena, such as entanglement and superdense coding, which enable powerful new methods of processing information that classical systems cannot easily replicate.

What Makes Quantum Computation Different?

Quantum computing fundamentally differs from classical computing in several key ways:

  1. Superposition and Infinite States: A single qubit can encode an infinite number of possible states, as opposed to the strict binary options of classical bits. This means quantum computers can process much more information simultaneously.
  2. Measurement and Probability: While qubits can exist in an infinite number of states, any attempt to measure them translates their quantum state into a classical state (either 0 or 1). This process is probabilistic. The quantum state does not yield a fixed answer but rather gives a probability distribution over possible outcomes, which makes quantum computing inherently different from the deterministic operations of classical systems.
  3. No Cloning: A vital distinction between classical and quantum systems is that qubits cannot be copied. In classical computing, data can be duplicated as needed, but in quantum systems, copying qubits requires measurement, which essentially destroys their quantum states. This limitation introduces significant challenges in designing quantum memory and other hardware.
  4. Quantum Speedup: While quantum computers still rely on classical control processors, they can solve certain types of problems far more efficiently. The sheer complexity of operations performed on multiple qubits in superposition provides a level of computational parallelism that classical systems can’t achieve. As a result, quantum computers can solve some problems exponentially faster than classical computers.

Classical vs Quantum: Two Types of Computation

In formal terms, classical computing is a subset of quantum computing. Quantum systems are, by their nature, a more general form of computation. A quantum computer can theoretically perform any task a classical computer can, but the reverse isn’t true.

While classical systems excel at deterministic, straightforward calculations and are still the most practical solution for everyday computing needs, quantum computers promise breakthroughs in fields like cryptography, materials science, and complex simulations. However, the full potential of quantum computing is still in its early stages, with numerous technical challenges remaining.

Conclusion

The advent of quantum computing represents a paradigm shift in how we think about computation. While classical computing remains essential for most of today’s technology, quantum computing opens the door to unprecedented computational power. By leveraging the principles of quantum mechanics, future quantum computers will tackle problems once deemed unsolvable, pushing the boundaries of science, technology, and innovation.

The light switch analogy offers a simple glimpse into the complexity of these two systems: while classical bits are limited to being ON or OFF, qubits, like points on the surface of a sphere, reveal an infinite range of possibilities. This difference is the key to quantum computing’s extraordinary potential. The future of computation lies in the quantum realm, where the probabilistic nature of the universe is harnessed for revolutionary breakthroughs.

Why is Quantum Computing So Hard to Understand?

Preface

As a software engineer, I recently embarked on a journey to learn quantum computing. The experience has been both fascinating and frustrating, as I found it incredibly difficult to come across simple explanations for this complex field. In light of this, I decided to start a “Simple Quantum Computing” series. Through this series, I aim to demystify quantum computing for myself and for you, dear reader, as we explore what could very well be the most revolutionary computing technology of the 21st century.

Introduction

Quantum computing represents the bleeding edge of technology. As software engineers, it is one of the most advanced areas we can dive into. Its potential impact on the world is enormous, particularly when it comes to cryptography. Quantum computers have already shown that they can break modern cryptographic algorithms with relative ease.

Despite its prospect, quantum computing has not yet gained widespread popularity. Unlike artificial intelligence, which saw a surge in interest with the advent of models like ChatGPT, quantum computing remains a niche area. This is partly due to the limited job market demanding quantum skills, but it is also due to the steep learning curve associated with the technology.

So, why exactly is quantum computing so hard to understand?

Analog Computational Values

As software engineers, we are used to binary computational values—0s and 1s. These discrete values form the foundation of classical computing, with processor words built from them, and every computation within a CPU is based on these simple, binary states.

Quantum Processing Units (QPUs), however, operate on a completely different principle. They use analog, or continuous, values instead of discrete ones. There is no clear-cut 0 or 1 in quantum computing; instead, everything is represented by complex numbers that can take on any value between the quantum states |0⟩ and |1⟩. These states are known as kets, a concept I will delve into further in a later post.

This shift from binary to analog values presents a significant challenge. As developers, we are not trained to work with these continuous values. For example, there is no straightforward equivalent of a boolean in quantum computing—no simple 0 or 1 to latch onto.

Moreover, modern QPUs must be controlled by classical CPUs. This means that the analog values computed by a QPU have to be translated into discrete values for the CPU to process. This translation introduces a loss of information and a level of unpredictability, as the process is not deterministic. The notion of working with probabilities—something that quantum computing heavily relies on—is foreign to most of us in the field of computer science, where even pseudo-random numbers are not truly random.

Overall, the different computational bases and the need to convert between them add a significant layer of complexity that we are simply not accustomed to dealing with.

Complex Mathematics

Another major hurdle in understanding quantum computing is the complex mathematics involved. To grasp the concepts of quantum computing, one must be comfortable with advanced mathematical theories and methods that go beyond basic arithmetic or algebra.

Some of the key mathematical areas used in quantum computing include:

  • Complex Numbers: One of the fundamental concepts is the extension of the real number system to include imaginary numbers. A complex number is expressed as a + bi, where a and b are real numbers, and i is the imaginary unit, satisfying i2 = −1.
  • Complex Analysis: This is the study of functions that operate on complex numbers. It includes topics such as analytic functions, complex integration, and conformal mappings.
  • Abstract Algebra: This area involves the study of algebraic structures like groups, rings, and fields. These structures are used to generalise algebraic concepts and solve equations in more abstract settings.

While it is possible to create simpler mathematical models by abstracting parts of this complex mathematics, understanding the original concepts is still crucial, and may be difficult (definitely for me!).

Programming a Processing Unit

Beyond the challenge of analog values and complex mathematics, there is the task of actually programming quantum processing units. Programming modern QPUs is reminiscent of programming early CPUs in the 1950s. If you compare quantum programming frameworks like Qiskit to early high-level programming languages like Autocode, you will notice striking similarities.

Both require you to think in terms of bits (or qubits), registers, memory, and gates. It is a very low-level form of programming that most modern software engineers are unaccustomed to. We lack the high-level frameworks and abstractions that we are familiar with in classical computing, making it even harder to get started in quantum computing.

Conclusion

Quantum computing represents a paradigm shift in how we approach computation, challenging many of the fundamental principles that have guided software engineering for decades. The complexity of analog computational values, the necessity to grasp advanced mathematics, and the low-level nature of quantum programming make it a field that is difficult to understand and master. 

However, these very challenges are what make quantum computing so promising and exciting. Just as classical computing evolved from low-level machine code to high-level languages and user-friendly frameworks, quantum computing will likely undergo a similar evolution. We are simply not there yet.

In this post series – simple quantum computing – I plan to write about different aspects of quantum computing that I found difficult to learn, and do my best to explain them in much simpler terms. I hope it will help both me and you understand what I believe to be the most promising computational technology of the XXI century. 

How to Implement the Pipes and Filters Architecture with Java and Azure

In my previous blog post, I provided an in-depth explanation of the Pipes and Filters architecture, which you can check out here. To recap, the Pipes and Filters architecture breaks down a system into small, self-contained processing components known as filters. Each filter is responsible for performing a specific task or transformation on the data it receives, promoting modularity and reusability. These filters are connected via pipes, which facilitate the flow of data from one filter to the next. This architecture is particularly effective in scenarios involving data integration, processing workflows, transformation pipelines, and stream processing.

In this blog post, I will walk you through a sample implementation of the Pipes and Filters architecture using Java and Azure. Our example project will centre around a chatbot designed to assist in creative thinking activities such as brainstorming.

Sample Project Overview

The goal of this project is to create a tool that integrates with a company’s creative thinking solutions. Specifically, it’s a chatbot that aids teams during brainstorming sessions and other creative activities. The process begins when a user interacts with the application by typing a question, such as “How will the ongoing AI revolution affect the financial industry?” This question is then sent to the application for processing.

How the System Works

  1. Input Validation: The first filter is responsible for validating the user’s question. The question might be in a language that the AI model doesn’t understand, or it might be too long or contain sensitive information. Therefore, the first task is to verify whether the question can be processed further.
  2. Prompt Engineering: If the question is valid, the application uses predefined templates to enrich it. These templates provide context to the AI-powered tool, making the model’s output more valuable. For example, a template might be: “You are a CEO. Given a strategic prompt, you will create X futuristic, hypothetical scenarios that happen Y years from now. The strategic prompt is: Z”. This step is crucial as it leverages prompt engineering to guide the AI model in generating more meaningful responses.
  3. AI Model Interaction: The final step involves sending the enriched prompts to the AI model, which processes them and generates answers. These answers are then displayed back to the user.

Implementation Details

The system consists of three filters:

  1. Input Validation Filter: Validates the user’s input according to the application’s data requirements.
  2. Prompt Engineering Filter: Analyses and enriches the validated input to create a prompt.
  3. AI Model Facade Filter: Sends the engineered prompt to the AI model and handles the response.

The First Filter: Input Validation

The first filter is implemented as an Azure Function, and its primary role is to validate the incoming question.

@FunctionName("QuestionValidationFunction")
public HttpResponseMessage validate(
       @HttpTrigger(name = "question",
               methods = {HttpMethod.POST},
               authLevel = AuthorizationLevel.FUNCTION)
       HttpRequestMessage<String> question,
       @QueueOutput(name = "questionQueue",
               queueName = "question-queue",
               connection = "AzureWebJobsStorage")
       OutputBinding<String> questionQueue,
       ExecutionContext executionContext) {
    // Implementation of validation.
}

The validate method, annotated with @FunctionName("QuestionValidationFunction"), is triggered by an HTTP request. It takes two parameters: the HTTP request containing the question and an output binding to a storage queue named "question-queue". The method validates the question and, if valid, sends it down the pipeline.

The Second Filter: Prompt Engineering

The second filter enriches the validated question with a template to maximise the AI model’s response quality.

@FunctionName("PromptEngineeringFunction")
public void sendPrompt(
       @QueueTrigger(
               name = "question",
               queueName = "question-queue",
               connection = "AzureWebJobsStorage")
       String question,
       @QueueOutput(
               name = "promptQueue",
               queueName = "prompt-queue",
               connection = "AzureWebJobsStorage")
       OutputBinding<String> promptQueue,
       ExecutionContext executionContext) {
   // Prompt engineering logic.
}

This function is triggered by messages in the "question-queue". When a new message arrives, the function is invoked, and the question is enriched before being sent to the next queue, "prompt-queue".

The Third Filter: AI Model Facade

The third filter handles communication with the AI model. This filter is implemented using the Spring Cloud Function framework, which decouples infrastructure configuration from the business logic. I’ll describe it in detail in the next blog post, but I’ll give you a short description here so you understand the code.

The functions are implemented as Java function interfaces and autowired into respective request handlers. The handlers contain logic that configures integration with the serverless platform provider. In our case it’ll be the Azure SDK (which examples we’ve seen before). With this setup, you can change the cloud provider by simply rewriting the handlers (and changing build definition) without any need to rewrite the functions itself. 

Let’s now look at the function’s code. 

@Bean
public Function<String, String> answer(ModelClient modelClient) {
   	// Function’s logic
}

The answer function is a simple Java function interface that handles the logic for interacting with the AI model. It is autowired into a handler that manages the integration with Azure.

@Component
public class AnswerHandler {

   private final Function<String, String> answer;

   public AnswerHandler(Function<String, String> answer) {
       this.answer = answer;
   }

   @FunctionName("answer")
   public void answer(
           @QueueTrigger(
                   name = "promptQueue",
                   queueName = "prompt-queue",
                   connection = "AzureWebJobsStorage")
           String prompt,
           @QueueOutput(
                   name = "answerQueue",
                   queueName = "answer-queue",
                   connection = "AzureWebJobsStorage")
           OutputBinding<String> answerQueue
   ) {
       // Handler’s logic
   }
}

This handler is similar to the previous filters, but it delegates the business logic to the answer function. The answerQueue is used to send the final answer for further consumption. 

Deployment

With all three filters implemented, you can now deploy the application to Azure, to play with the code. The deployment process can be accomplished using Maven, as described in this article

In summary, we implemented a complete Pipes and Filters architecture using both the Azure SDK and Spring Cloud Function. The system comprises three filters – each responsible for a distinct part of the application’s workflow: input validation, prompt engineering, and AI model communication. The unidirectional data flow is managed primarily by queues, ensuring a clean separation of concerns and easy scalability.

Summary

This blog post demonstrates how to implement the Pipes and Filters architecture using Java and Azure for a chatbot that assists in creative thinking activities. The architecture is broken down into three filters: input validation, prompt engineering, and AI model interaction. Each filter handles a specific task in the data processing pipeline, ensuring modularity and reusability. The post also covers the deployment process using Azure and Spring Cloud Function, highlighting the benefits of separating business logic from infrastructure configuration.

If you’re interested in how this architecture style can be used to implement serverless solutions, and how to work with Azure Functions in Java, check out my Udemy course that covers these topics in detail.  

The working code example can be found on my GitHub

Serverless with the Pipes and Filters Architecture

In a previous blog post I argued that serverless computing is not an architecture style. However, I made a point there that – as with every technology – some architecture styles go along with it better than the others. What most probably comes to your mind are microservices and event-driven architecture. In this blog post though, I would like to cover a less known architecture style that seems to perfectly match serverless computing – the pipes and filters architecture, which emphasises modularity and reusability in processing components. This architecture is built on the concept of “pipes” that connect filters, enabling the transformation or processing of data as it flows through the system. 

Understanding Pipes and Filters Architecture

The Pipes and Filters architecture is characterised by decomposing a system into small, self-contained processing components called filters. Each filter performs a specific task or transformation on the data it receives, promoting a highly modular and reusable design. Filters are connected using pipes, which serve as conduits for data flow, ensuring a one-way flow of information through the system. This architecture is particularly useful in scenarios such as data integration, data processing workflows, data transformation pipelines, and stream processing systems.

Key features of the Pipes and Filters architecture include:

  • Modularity and Reusability: Each filter is designed to be independent, reusable, and replaceable. This modularity allows for flexible composition of processing components.
  • Sequential Data Flow: Data passes from one filter to the next through pipes, enabling sequential processing.
  • Loose Coupling: Filters interact through well-defined data interfaces provided by the pipes, promoting loose coupling and reusability.
  • Scalability and Parallelism: Filters can be replicated or distributed to handle increased processing loads, allowing for parallel processing.
  • Flexibility and Adaptability: Filters can be added, removed, or rearranged within the pipeline to accommodate changing processing needs.

Serverless Pipes and Filters

Serverless computing naturally complements the Pipes and Filters architecture through several key features. One significant aspect is the modularity and reusability inherent in both paradigms. In serverless computing, individual functions are designed to perform specific tasks and can be independently deployed, updated, or replaced without impacting the rest of the system. This mirrors the independent and reusable nature of filters in the Pipes and Filters architecture, where each filter is a standalone processing unit. Additionally, serverless platforms inherently support sequential data flow through event-driven triggers, similar to how data flows through pipes from one filter to another. This ensures that each function or filter performs its task in sequence, enhancing the clarity and manageability of the data processing pipeline. Moreover, serverless functions communicate through well-defined event interfaces, which aligns with the loose coupling seen in the Pipes and Filters architecture. This separation allows for easier maintenance and testing, as changes in one function or filter do not directly affect others. The scalability and parallelism provided by serverless architectures are also a perfect match for the scalable and distributable nature of filters, allowing the system to handle varying loads efficiently. Finally, the flexibility and adaptability of serverless functions, which can be quickly modified or scaled, resonate with the ability to add, remove, or rearrange filters within the pipeline, making it easy to adapt to changing requirements or workloads.

Functions Chaining

There is one special feature of serverless computing that goes particularly well with the pipes and filters architecture – functions chaining. It is the practice of linking multiple serverless functions together, where the output of one function serves as the input for the next. This allows for the creation of complex workflows by decomposing tasks into smaller, manageable, and reusable functions that execute sequentially. It mirrors exactly how a pipes-and-filters application should be designed, and that is the main reason why serverless computing is my go-to technology whenever I use the pipes and filter architecture in my design.

Summary

In this blog post, we explored the synergy between serverless computing and the pipes and filters architecture, a lesser-known but highly effective design that emphasises modularity and reusability in processing components. The pipes and filters architecture decomposes systems into self-contained filters connected by pipes, ensuring a sequential data flow. Serverless computing complements this by offering modular, independently deployable functions that support sequential processing and loose coupling through well-defined event interfaces. This combination enhances scalability, parallelism, and flexibility, making it ideal for dynamic workloads. A key feature of serverless computing, function chaining, perfectly aligns with the pipes and filters model, enabling the creation of complex, manageable workflows.

In the next blog post I will show you in example how serverless computing can be used in such a design.

Generating Avro Schemas in Kotlin

When working with Avro schemas, the most popular approach is to generate Java or Kotlin classes based on predefined Avro schemas. This method is particularly useful if your application acts as a consumer or if the schemas are provided by someone else. However, if you are the sole publisher for a topic, you might prefer to generate Avro schemas directly from your model classes.

A handy tool for this task is the avro4k library. This third-party library simplifies the process of generating Avro schemas from Kotlin data classes. It leverages the powerful kotlinx.serialisation library, making the integration straightforward and efficient.

The kotlinx.serialisation library is well-documented and provides extensive resources to help developers get started. You can explore its official documentation here. However, a significant limitation is that the provided setup guides are primarily focused on Gradle. If you are using Maven (as I usually do), you might find the lack of specific instructions frustrating.

In this tutorial, I will guide you through setting up a Maven build, writing a simple Kotlin model, and generating an Avro schema from it using avro4k and kotlinx.serialisation.

Setting Up a Maven Project for Kotlinx serialisation and Avro4k

In this section, I will walk through the steps to set up a Maven project for generating Avro schemas from Kotlin data classes using the kotlinx.serialisation library and the avro4k library. We will start by creating a new Maven project, configuring the necessary dependencies, and finally setting up the Kotlin Maven Plugin.

Step 1: Create a New Project from Maven Archetype

First, create a new Maven project using the Kotlin archetype org.jetbrains.kotlin:kotlin-archetype-jvm with version 2.0.0. This archetype provides a basic project structure for Kotlin applications.

Step 2: Replace JUnit 4 with JUnit 5

The default setup includes JUnit 4, which we need to replace with JUnit 5 to take advantage of the latest testing features. Let’s update your pom.xml to include the JUnit 5 dependency.

Step 3: Add the avro4k Dependency

Next, add the avro4k dependency to your pom.xml.

<dependency>
<groupId>com.github.avro-kotlin.avro4k</groupId>
<artifactId>avro4k-core</artifactId>
<version>${avro4k-core.version}</version>
</dependency>

The variable avro4k-core.version points to version 1.10.1.

This library contains all the logic that we need to generate Avro schemas directly from Kotlin data classes.

Step 4: Configure the Kotlin Maven Plugin

Now, configure the Kotlin Maven Plugin to include the kotlinx.serialisation compiler plugin. Add the following configuration to your pom.xml:

<build>
    <sourceDirectory>src/main/kotlin</sourceDirectory>
    <testSourceDirectory>src/test/kotlin</testSourceDirectory>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>${kotlin-dependencies.version}</version>
            <executions>
                <execution>
                    <id>compile</id>
                    <phase>compile</phase>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
                <execution>
                    <id>test-compile</id>
                    <phase>test-compile</phase>
                    <goals>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <languageVersion>${kotlin.version}</languageVersion>
                <jvmTarget>${java.version}</jvmTarget>
                <compilerPlugins>
                    <plugin>kotlinx-serialization</plugin>
                </compilerPlugins>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-serialization</artifactId>
                    <version>${kotlin-dependencies.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Two parts of this configuration are crucial for setting up Maven to work properly with Kotlinx serialisation: compilerPlugins and their dependencies.

First, you have to add the kotlinx-serialization plugin to the compilerPlugins section. It specifies additional compiler plugins to be used during the compilation process that will enhance or modify the compilation. However, it is just a declaration. It will not automatically download necessary dependencies.

In order to configure it as well, you have to add the following definition to the dependencies section of the plugin configuration.

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-serialization</artifactId>
    <version>${kotlin-dependencies.version}</version>
</dependency>

This setup ensures that the Kotlin serialisation plugin is correctly applied during the build process.

Step 5: Re-import the Project in IntelliJ IDEA

If you are using IntelliJ IDEA as your development environment, re-import the Maven project to apply the new configurations. Occasionally, IntelliJ IDEA’s compiler might not recognize symbols from the Kotlin serialisation library. If this happens, invalidate caches and restart the IDE.

Implementing a Simple Model for Avro Serialisation

To demonstrate the use of Kotlinx Serialization and Avro4k, let’s implement a simple model for a book. For simplicity, we will keep all the related classes in one file since the model is not very big. Here is how you can define the necessary classes:

enum class CurrencyType {
    USD, EUR, NOK // Can be extended if needed.
}

data class Price(
    @Serializable(with = BigDecimalSerializer::class) var amount: BigDecimal,
    var currency: CurrencyType
)

data class Author(
    val firstName: String,
    val lastName: String
)

data class Book(
    val title: String,
    val author: Author,
    @Serializable(with = YearSerializer::class) val publicationYear: Year,
    val numberOfPages: Int,
    val price: Price,
    @AvroFixed(13) val isbn: String
)

In this model:

  • CurrencyType is an enum class representing the currency type for the book price. It includes USD, EUR, and NOK but can be extended if needed.
  • Price is a data class that holds the amount and currency type of the book’s price.
  • Author is a data class that contains the first and last name of the book’s author.
  • Book is a data class that includes details such as the title, author, publication year, number of pages, price, and ISBN of the book.

Having the model in place, we can add the @Serializable annotation to the signature of every class, like in the following example.

@Serializable
data class Book

Custom Serializers for Unsupported Types in Kotlinx Serialization

When working with Kotlinx serialisation, you might encounter types that do not have default serialisers provided by the library. In our book model, the Year and BigDecimal classes fall into this category. To handle these types, we need to implement custom serialisers. Here is how you can do it.

Every custom serialiser must implement KSerializer interface, providing custom logic for the descriptor field, as well as the deserialize and serialize functions. Let’s do it for the Year type.

class YearSerializer : KSerializer<Year> {

    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor("YearSerializer", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): Year {
        return Year.parse(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: Year) {
        encoder.encodeString(value.toString())
    }
}

We started with specifying the descriptor: SerialDescriptor value to describe the serialised form as a string. It is needed by the deserialiser to properly assign Avro type to a given value. In the deserialize method, it converts a string back into a Year object using Year.parse. Conversely, the serialize method transforms a Year object into its string representation. This custom serialiser ensures that Year values are properly converted to and from their string forms during serialisation and deserialisation processes.

Similarly, we can implement the serialiser for the BigDecimal type.

class BigDecimalSerializer : KSerializer<BigDecimal> {
    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): BigDecimal {
        return BigDecimal(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: BigDecimal) {
        encoder.encodeString(value.toString())
    }
}

Updating the Model Classes

With the custom serialisers implemented, the next step is to update the respective fields in our model classes to use these serialisers. In order to do so, you have to add two annotations to members of the Book and the Price classes.

Let’s start with the Book class.

@Serializable
data class Book(
    val title: String,
    val author: Author,
    @Serializable(with = YearSerializer::class) val publicationYear: Year,
    val numberOfPages: Int,
    val price: Price,
    val isbn: String
)

And then move to the Price class.

@Serializable
data class Price(
    @Serializable(with = BigDecimalSerializer::class) var amount: BigDecimal,
    var currency: CurrencyType
) 

The @Serializable annotation can be used for fields too, and allows to specify a custom serialiser for a given one.

Writing a Test to Generate and Validate the Avro Schema

With our model and custom serializers set up, we are ready to write a test to generate the Avro schema from the Book class and validate it against the expected schema. This process ensures that the schema generated by the avro4k library matches our predefined expectations.

Step 1: Define the Expected Schema

First, we define the expected Avro schema as a JSON string. This schema should reflect the structure of our Book data class, including nested fields and custom serializers.

val expectedSchema = """
            {
              "type" : "record",
              "name" : "Book",
              "namespace" : "com.lukaszpalt",
              "fields" : [ {
                "name" : "title",
                "type" : "string"
              }, {
                "name" : "author",
                "type" : {
                  "type" : "record",
                  "name" : "Author",
                  "fields" : [ {
                    "name" : "firstName",
                    "type" : "string"
                  }, {
                    "name" : "lastName",
                    "type" : "string"
                  } ]
                }
              }, {
                "name" : "publicationYear",
                "type" : "string"
              }, {
                "name" : "numberOfPages",
                "type" : "int"
              }, {
                "name" : "price",
                "type" : {
                  "type" : "record",
                  "name" : "Price",
                  "fields" : [ {
                    "name" : "amount",
                    "type" : "string"
                  }, {
                    "name" : "currency",
                    "type" : {
                      "type" : "enum",
                      "name" : "CurrencyType",
                      "symbols" : [ "USD", "EUR", "NOK" ]
                    }
                  } ]
                }
              }, {
                "name" : "isbn",
                "type" : {
                  "type" : "fixed",
                  "name" : "isbn",
                  "size" : 13
                }
              } ]
            }
        """.trimIndent()

Step 2: Generate the Schema from the Model

Next, we generate the actual Avro schema from the Book class using the avro4k library.

val actualSchema = Avro
    .default
    .schema(Book.serializer())
    .toString(true)

This code uses the Avro.default.schema method to generate the schema and converts it to a pretty-printed JSON string for easier comparison.

Step 3: Assert the Schemas Match

Finally, we assert that the generated schema matches the expected schema.

assertEquals(expectedSchema, actualSchema)

Summary

Generating Avro schemas directly from Kotlin data classes is made straightforward with tools like avro4k and kotlinx.serialization. By setting up a Maven project and configuring the Kotlin Maven Plugin, you can seamlessly serialize Kotlin classes into Avro schemas. This approach simplifies integration, especially when you’re the sole producer for a given topics or you define a model for a given domain.

The avro4k library is quite powerful and allows more than I demonstrated in this tutorial. You may be particularly interested in the following sections of its documentation.

  • Schema definition options.
  • Serialisation and deserialisation options.

You can find the complete working code for this tutorial in my GitHub repository.

Understanding Serverless Computing. Not Just an Architecture Style

Understanding IT architecture styles is crucial for anyone involved in the design and development of software systems. These styles provide a framework of principles, patterns, and guidelines that shape the structure, organisation, and behaviour of IT applications. However, there is often confusion between architectural styles and deployment models, with serverless computing frequently misinterpreted as an architecture style. It is quite common to include serverless as a new architecture style, along microservices or event-driven architecture. In this article I aim to clarify this misconception, emphasising that serverless is primarily a deployment model rather than an architectural style. By exploring the comprehensive role of architectural styles and the distinct nature of serverless computing, I will highlight why understanding this distinction is essential for effectively leveraging serverless in your IT solutions.

What is an IT Architecture Style?

An IT architecture style is a comprehensive set of principles, patterns, and guidelines that define the overall structure, organisation, and behaviour of an information technology system or application. It provides a high-level framework for designing, developing, and deploying IT solutions. Regardless of the specific architectural style you apply, they all share several common features.

  • Principles and Concepts. A software architecture style defines a set of principles, concepts, and practices that guide the design of software systems. These principles and concepts provide a common vocabulary and shared understanding among software architects.
  • Structural Elements. It defines the structural elements of a software system, such as components, modules, and layers, organised in a way that promotes modularity, flexibility, and scalability.
  • Patterns and Templates. Architecture styles typically provide patterns and templates that help architects make decisions about system structure and component usage.
  • Implementation Technologies. An architecture style may specify particular implementation technologies, such as programming languages, frameworks, or libraries, chosen based on their suitability for the style.
  • Quality Attributes. They consider the quality attributes of a software system, such as performance, reliability, maintainability, and security, providing guidelines to meet these attributes.
  • Trade-offs. Architecture styles often involve trade-offs between different quality attributes or design goals, such as prioritising performance over flexibility or security over ease of use.
  • Standards and Conventions. They include standards and conventions for naming, coding style, documentation, and other aspects of software design, ensuring consistency and maintainability across the system.

Serverless Computing: A Deployment Model, Not an Architecture Style

Having defined what an architectural style encompasses, let’s explore the nature of serverless computing and how it differs fundamentally from an architecture style.

  • Deployment Model. Serverless computing is primarily a deployment model. It is often associated with “Functions as a Service” (FaaS) platforms, where developers deploy individual functions to the cloud, and the cloud provider manages the scaling and underlying infrastructure. This model focuses on how the system is deployed and managed, rather than how it is designed and structured.
  • Scope of Address. While serverless computing offers benefits such as scalability, cost-effectiveness, and increased developer productivity, it doesn’t address all aspects of architecture. For instance, it does not provide solutions for data management, security, or integration with other systems.
  • Compatibility with Various Architectures. Serverless can be used with various architectural styles, such as microservices, event-driven architecture, or even hexagonal architecture (though it is a poor choice for going serverless). This flexibility indicates that serverless is not inherently tied to any specific architecture but can be used in different contexts depending on the system’s needs.
  • Vendor Lock-in. Serverless computing often relies on proprietary services and APIs from cloud providers, which can lead to vendor lock-in. This dependency can make it challenging or costly to switch providers or bring the system in-house.

The Distinction: Architecture Style vs. Deployment Model

An architectural style provides a comprehensive template for designing your entire IT system, irrespective of its size. It encompasses a broad set of principles and guidelines for system structure, component organisation, and quality attributes. Serverless computing, on the other hand, is a runtime environment to which you can deploy parts of your application. It serves as an implementation platform for applications designed using a particular architectural style.

In summary, while serverless computing offers a powerful and flexible deployment model, it does not fulfil the role of an architectural style. Instead, it complements various architectural styles by providing a scalable, cost-effective environment for running application components. Understanding this distinction is crucial for effectively leveraging serverless computing in your IT solutions.

Deploy Azure Functions with Maven

Azure Functions is a serverless compute service provided by Microsoft Azure, designed to help developers run event-driven code without the need to manage infrastructure. Whether you’re automating workflows, building APIs, or processing data streams, Azure Functions offers a scalable, cost-effective solution. By abstracting away the complexities of server management, it allows developers to focus on writing the logic that drives their applications.

For Java developers, Azure Functions offers first-class support, making it easier than ever to integrate Java-based solutions into the Azure ecosystem. This support includes a robust set of tools and libraries specifically designed to streamline the development and deployment process for Java applications. If you’re eager to learn more, Microsoft has prepared a decent documentation covering this topic. You’ll find there information about the programming framework, how to implement a Function, and how to deploy it. In this post I’d like to take a look at the latter and present it to you in a slightly different light than usual.

Traditionally, deploying Azure Functions involves tools like Azure CLI, Bicep, or Terraform. These tools are robust and widely used, offering powerful features for managing Azure resources. However, Java developers also have a more seamless and better integrated option to choose from – Maven.

Maven, a build automation tool primarily used for Java projects, can be an excellent choice for deploying Azure Functions. The Azure Functions Maven Plugin allows developers to define their infrastructure as code, simplifying the deployment process and ensuring consistency across environments. It’s a great choice if you’re starting your journey with Azure Functions or have no other infrastructure in the cloud. It’s easy to use and allows you to keep your infrastructure definition as close to your build definition as possible.

The Azure Functions Maven Plugin allows you to use an infrastructure-as-code approach similar to Bicep or Terraform. You can declare the desired infrastructure, and the plugin handles provisioning and state management. This means your Function App won’t be reprovisioned every time you build your application, saving time and resources. While this might seem abstract, seeing it in action will clarify its efficiency and ease of use. Let’s dive into the practical aspects of deploying your Azure Functions with Maven and see how this approach can streamline your development process.

I’m using Maven v3.9.6 in this tutorial. You can find a working example of this configuration here.

First, let’s open the pom.xml file in the azure-funcitons-sdk module. Within this file, you’ll find the declaration of the azure-functions-maven-plugin. At the core of this declaration is the <plugin> element. In Maven, a plugin is a collection of goals, which are specific tasks or actions. In our case, we’re using the azure-functions-maven-plugin to facilitate the deployment of Azure Functions. Let’s take a look at the definition. 

<plugin>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>azure-functions-maven-plugin</artifactId>
    <version>${azure.functions.maven.plugin.version}</version>
    <configuration>
        <appName>azure-functions-sdk</appName>
        <resourceGroup>azure-functions-sdk-rg</resourceGroup>
        <appServicePlanName>azure-functions-sdk-sp</appServicePlanName>
        <pricingTier>Consumption</pricingTier>
        <region>norwayeast</region>
        <runtime>
            <os>linux</os>
            <javaVersion>17</javaVersion>
        </runtime>
        <appSettings>
            <property>
                <name>FUNCTIONS_EXTENSION_VERSION</name>
                <value>~3</value>
            </property>
        </appSettings>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>package</goal>
            </goals>
        </execution>
    </executions>
</plugin>

There’s a lot of things you might be unfamiliar with in this declaration, so let’s crack them one by one.

Plugin Coordinates

(This is pretty standard, but let’s quickly go through them, especially if you’re new to Maven.)

  • <groupId>: Specifies the group identifier for the plugin. In this case, it’s com.microsoft.azure.
  • <artifactId>: Identifies the plugin itself. We’re using the azure-functions-maven-plugin.
  • <version>: Specifies the version of the plugin. The ${azure.functions.maven.plugin.version} variable points to a specific version, such as 1.24.0 in our example.

Configuration Section

  • <appName>: The name of your Azure Functions application, set to azure-functions-sdk.
  • <resourceGroup>: Defines the Azure resource group where your functions will be deployed, here named azure-functions-sdk-rg.
  • <appServicePlanName>: The Azure App Service Plan where your functions will run, set to azure-functions-sdk-sp.
  • <pricingTier>: Specifies the pricing tier for your functions. It’s set to Consumption, since it’s the cheapest and most popular option. You can also choose Premium or Dedicated tier here.
  • <region>: Determines the Azure region for deployment, set to norwayeast in our case. Note that free trial subscriptions may have limitations on available regions. Check Azure’s documentation for your specific options.
  • <runtime>: Configures the runtime settings:
    • <os>: Specifies the operating system, which is linux.
    • <javaVersion>: Sets the Java version used for the functions runtime, set to 17.
  • <appSettings>: Defines application settings specific to your functions. They’re populated as environment variables to your Function runtime. Here we only specify the Functions version itself, but you can put active profiles or other environment-dependent key-value pairs here.

Executions Section

  • <execution>: Defines when and how the plugin’s goals should be executed. The package goal is responsible for packaging your Azure Functions for deployment. Note that this phase does not deploy anything to Azure; it merely prepares the package. The actual deployment requires invoking a plugin-specific goal, which we will cover later.


Great! Now that we have our configuration in place, the next step is to run the build and deploy our Azure Functions. First, we need to prepare the artefact by executing the clean package command. You can do this from the right-hand side menu in IntelliJ IDEA. This step takes a few seconds to complete on my machine.

Once the packaging is done, the next step is to execute a plugin-specific goal: deploy. It’s essential to use the correct deployment goal to ensure our code gets deployed to Azure. By default, Maven’s deploy goal sends artefacts to the artefact repository specified in the <distributionManagement> section of your POM file, such as Nexus. However, we want to deploy our code directly to Azure. To do this, we need to specify the deployment goal from the Azure Functions Maven Plugin.

Before proceeding, ensure you have Azure CLI installed on your machine. The installation steps vary depending on your operating system, and you can find detailed instructions in the Azure CLI installation guide. I used Homebrew to install it on my machine, but you can choose any way that suits you best.

Once you have Azure CLI installed, open the terminal and type az login. This command initiates a session from your local machine to Azure’s REST API, which is necessary for the plugin to function correctly.

Now, let’s start the deployment process. Issue the following command from the directory where the POM file is located:

mvn azure-functions:deploy

As the deployment progresses, you’ll see logs indicating that all the necessary infrastructure is being provisioned, including the Resource Group, Function App, Application Insights, Storage Account, and App Service Plan. Once the deployment is complete, you can navigate to the Azure Portal to verify that everything is correctly set up and running. This is a great feature itself, because using generic tools like Bicep forces you to define related resources independently. Moreover, all necessary secrets (like connection string to the Storage Account) are added to your App Settings.

And that’s it! By following these steps, you have successfully deployed your Azure Functions using Maven. It may feel a bit odd in the beginning, especially if you’re already experienced with Bicep or Terraform, but I find this approach quite useful in scenarios when I don’t have a lot of infrastructure in Azure, or it’s provided by an external team. In this case an ability to define my Function App in the POM file and use the same tool for build, test, and deployment contributes to a great developer experience. Moreover, automatic creation of all related resources and configuration allows me to get the function up and running in a couple of minutes, making the Maven-based deployment a great option for learning and prototyping.