
[{"content":"","date":"7 April 2026","externalUrl":null,"permalink":"/","section":"","summary":"","title":"","type":"page"},{"content":"","date":"7 April 2026","externalUrl":null,"permalink":"/tags/crosspost/","section":"Tags","summary":"","title":"Crosspost","type":"tags"},{"content":"This post appeared first on factor10.com.\nIt just doesn\u0026rsquo;t make any sense! Your software team has been getting slower and slower up to a point where every new feature is painfully forced through the system, wrecking everything it touches, leaving everyone afraid to make any changes in the first place and adding more and more defensive processes that slow the system down additionally.\nThe software team says the workload is too high. So you add more people. That should help! But what happens instead is that features now take even more time to squeeze through the system and your people start blaming each other. What has happened?\nYou are far from the first person to live through this dilemma, and the good news is that there are ways to add more people to a project and actually go faster, but it requires some serious work. In 1975 Fred Brooks wrote about this conundrum in his book \u0026lsquo;The Mythical Man Month\u0026rsquo;, where he compares developing a software project with having a baby. You won\u0026rsquo;t be able to get that baby in one month when you assign nine people to making it. He coined Brooks\u0026rsquo;s Law which says that \u0026ldquo;Adding manpower to a late software project makes it later.\u0026rdquo; Several other people have made the same discovery since and came up with reasons and solutions (for the software problem, not the baby one).\nHere is a list of some of the reasons:\n1. Communication gets more complicated # For every person added to a team, the number of communication channels increases quadratically. If you had five people in the team, there were 4 + 3 + 2 + 1 = 10 communication channels. If you add a sixth person, there are now five more (5 + 4 + 3 + 2 + 1 = 15) communication channels. Each new person increases the number of channels by the number of persons already in the team.\nOfficial communication becomes a lot harder, but the unofficial communication suffers too. Situations like overhearing your colleague talking about a problem that you know how to solve become more unlikely the more colleagues there are.\nCommunication through code suffers as well. With fewer people it is easier to decide on shared standards and to have discussions about the structure of the code. With a lot more people involved, it is harder to maintain conceptual integrity: the code won\u0026rsquo;t look like it was written by one person and thus be harder to navigate, understand, change and maintain.\n2. Lack of trust # The authors of the book Team Topologies argue that the size of a team is limited by Dunbar\u0026rsquo;s number, which is the maximum number of people with whom one can maintain stable social relationships.\nIf the team gets too big (more than nine members), the trust automatically decreases because of this human limitation. High trust is what allows teams to innovate and evolve. If it is lacking or reduced, the speed and quality of delivery will decrease.\n3. Onboarding takes time # It takes time to onboard new members, and it takes away time from those who are familiar with the system. This is why adding people to an already late project makes it later.\nIf you, however, add more people before everything is on fire or if you add the right and highly qualified people, Brooks\u0026rsquo;s Law does not apply (according to Brooks). It also does not apply if you can perfectly partition the tasks, which Brooks argues is very rare.\n4. The real problem is the cognitive load # The cognitive load remains the same even if you add more people, you cannot decrease it by sharing it (it is like \u0026lsquo;fun\u0026rsquo; in that way, only much less fun). Complicated code, processes and tools will influence the speed of every team member, if there are more, that just means more people are trying to make sense of the same mess. Cognitive overload can also be rooted in teams being responsible for too many domains or an architecture that doesn\u0026rsquo;t reflect the organizational structure.\n5. The architecture does not support it # There\u0026rsquo;s another law called Conway\u0026rsquo;s Law that says that the architecture of your systems prevails the structure and communication paths within your organization. So adding more people to a big team will not only complicate the communication and trust but also the architecture. If you try to change the architecture without reorganizing, this will, according to Conway and Team Topologies, only lead to friction and cognitive overload, and in the end the organizational structure will prevail over the architecture again.\nWhat to do? # So what needs to be in place to be able to gain speed by adding people? According to Conway\u0026rsquo;s Law, you should start with working on the organizational structure. Together with technical people who know the current architecture and its limitations, you can reorganize with the goal to improve the architecture (this is called an Inverse Conway Maneuver). Taking trust and cognitive load into consideration, you should make sure that the teams stay small and can work independently of each other. Aim for one Bounded Context per team. Now you can make sure that the architecture reflects the organizational changes. This is called a sociotechnical process because it requires both social and technological change.\nThe new organizational and architectural structure (which is modular now) makes it much easier to add more people and gain speed.\nOnce you have come so far, you might find that you no longer need to add more people to gain speed. But if you decide to do so, you are now able to add a whole new team that starts working on a new module without affecting other teams. Or you add multiple new people to a bunch of smaller teams. If each team has to onboard one new person, the negative consequences (trust, communication) are much less serious than onboarding a lot of new people into an already big team. You can also think about adding people with the purpose to remove obstacles for the team (a so-called enabling team according to Team Topologies) and thus speed up all other delivery teams.\nYou still won\u0026rsquo;t be able to make a baby in a month, but you might be able to make more babies in 9 months than you could imagine.\n","date":"7 April 2026","externalUrl":null,"permalink":"/post/2026-04-crosspost-more-members-slower-delivery/","section":"Posts","summary":"This post appeared first on factor10.com.\nIt just doesn’t make any sense! Your software team has been getting slower and slower up to a point where every new feature is painfully forced through the system, wrecking everything it touches, leaving everyone afraid to make any changes in the first place and adding more and more defensive processes that slow the system down additionally.\n","title":"Crosspost: Why is my software development team slow despite having more people?","type":"post"},{"content":"","date":"7 April 2026","externalUrl":null,"permalink":"/post/","section":"Posts","summary":"","title":"Posts","type":"post"},{"content":"","date":"7 April 2026","externalUrl":null,"permalink":"/tags/software-engineering/","section":"Tags","summary":"","title":"Software Engineering","type":"tags"},{"content":"","date":"7 April 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/tags/ddd/","section":"Tags","summary":"","title":"Ddd","type":"tags"},{"content":"In the blogpost about structural idealization I explained how structural engineers model reality to answer the question whether their designed structure can withstand the forces applied to it. But how does an engineer know which forces will be applied to their structure? The weight of the structure can be approximated at best, and natural occurrences like wind or snow or a car crashing into the building are much harder to anticipate and quantify. And how does an engineer consider a car crashing into the structure while still building reasonably economical?\nIn this blogpost I\u0026rsquo;ll give you an introduction into how I as Structural Engineering student was taught to tackle this challenge. By drawing parallels between Structural Engineering and Software Engineering I\u0026rsquo;ll try to answer the question: What can Software Engineering learn from Structural Engineering regarding reliability?\nStructural Engineers use Limit State Design to express the structural reliability of a building with regards to so called Limit States.\nLimit States # Limit States are boundaries that a building may not cross. Beyond the Limit States the structure has lost stability, has endangered human life or is simply no longer usable.\nLimit State Design compares the Effect of \u0026lsquo;actions\u0026rsquo; (= loads) to the Resistance of materials (which will be another post) and the structure. The goal is to verify that the loads never exceed the resistance, keeping the structure below the limit at all times and in all relevant situations.\nUltimate Limit States # Ultimate Limit States are states associated with collapse or other similar forms of structural failure that endanger the safety of people, and/or the safety of the structure. The actual Ultimate Limit States that need to be taken into consideration can differ from structure to structure and from surroundings to surroundings.\nExamples of Ultimate Limit States include a footing that slides, a column that buckles or a roof that collapses.\nUltimate Limit States focus on safety, on the structure unintentionally becoming a threat. Examples for States where a Software System unintentionally and without malicious intent becomes a threat are a higher traffic than anticipated, bringing the server to its knees, cascading failures resulting from one failing service (availability), a corrupted database (integrity) or accidentally granting users more rights than they should have (confidentiality).\nSecurity, which is usually not a priority for the structural design of buildings, is a priority for Software Systems. Security introduces a distinct category of Ultimate Limit States, those caused by deliberate action rather than accident such as DOS attacks (availability), someone reading data in transit (confidentiality), someone maliciously changing data records in a database (integrity) and other intentional attacks.\nWhether the cause is accidental or deliberate, the effect is the same: a destabilized system. As is the goal of both Structural Engineering and Software Engineering faced with Ultimate Limit States: preventing the system from causing harm by maintaining system stability.\nServiceability Limit States # Serviceability Limit States are all states that touch the usability of a structure under normal use.\nExamples for problems resulting from exceeding a Serviceability Limit state are machines in the building that can no longer operate because the floor is too deformed, or because of excessive vibrations. The Serviceability Limit State is also exceeded when people perceive the building as unsafe. One cause can be deformations, that are not safety-critical (they are considered safe in the Ultimate Limit State verifications), but large enough to make people feel uneasy.\nLarge vibrations that have no effect on the stability of the system can cause discomfort in people too and need to be limited to comply with the Serviceability Limit State verifications as well.\nOnly satisfying the Ultimate Limit State is not sufficient. Both Limit States must be satisfied for the intended use of the structure.\nEven in Software Engineering your system needs to be secure, stable AND useable as intended.\nIt is not sufficient that the system is secured and availability and uptimes are great. \u0026lsquo;Nines don\u0026rsquo;t matter when users aren\u0026rsquo;t happy\u0026rsquo;, meaning that it doesn\u0026rsquo;t matter how stable a system is, if the users cannot use it as they intended. There are many different reasons why users aren\u0026rsquo;t happy, the latency can be too high, the navigation is unintuitive, the application has accessibility issues making it unusable for a group of people, the error messages are meaningless, or a button is placed somewhere where the user cannot find it. It is enough for users to simply dislike the experience for the whole system to be seen as a failure.\nDetermining Loads # In order to quantify the loads the Structural Engineer needs to know what situations to consider and for how long the building needs to withstand the actions.\nDesign Working Life # Structural Engineers pick a reasonable Design Working Life for the structures they design. For a festival stage, that is supposed to stand for a month the probability that it is hit by a once-in-100-years storm is much lower than it is for a permanent structure like a bridge. So generally, the intensity of environmental loads (like wind or snow) can be adjusted based on the structure’s lifetime.\nIn Software Engineering it helps to determine the Design Working Life of a system and adjust the robustness and maintainability of the system accordingly. A short-lived application can afford to optimise for speed of delivery. A long-lived one needs to optimise for the people who will maintain it, who may not be the same people who built it.\nSo determining the Design Working Life of an application and communicating it clearly is important to finding that balance between reliability and economics.\nDesign situations # A Design Situation describes a situation the structure can be in, that needs to be considered during design.\nDesign Situations are grouped in categories, most interesting in the context of Software Engineering is the distinguishing between the Persistent and Accidental Design Situations. So that is what I\u0026rsquo;ll concentrate on.\nThe Persistent Design Situation refers to the normal use of the structure from when it is completed to the end of its Design Working Life. Every load that is likely to occur under normal use needs to be considered. So for a house with a Design Working Life of 50 years the once-in-50-years storm is found from history data and used as a load that the structure must endure.\nTo determine the design loads for the Ultimate Limit States and the Persistent Design Situation the expected loads are multiplied by a safety factor greater than one, creating a deliberate margin above what is actually anticipated.\nFor the Serviceability Limit States and the Persistent Design Situation (usability under normal use) however the actual loads are not being increased with a partial safety factor.\nThe Accidental Design Situation deals with exceptional situations, like fires, explosions or human errors. Because it is unlikely that the 50 year storm and the Accidental Situation occur at the same time the actual wind loads (and other environmental loads) are decreased for the verification. Actual loads are generally not being increased with a partial safety factor in the accidental design situation either. It is important that the structure does not collapse and that is what is designed for in this Design Situation. It is accepted that the building can not be used as intended when an accidental situation occurs.\nThis approach adds a safety net where it counts. Still designing for useability and accidents, but intentionally accepting a higher reliability risk to build economically.\nIn Software Engineering we can apply the same thinking. We can do so by adding a margin to what we expect our system to be necessary to handle. We can for example make sure it is easily horizontally scalable, we can build in redundancy, an error queue and retries. For accidental situations (a DOS attack or a security breach), we accept that the system cannot function as intended. But we deliberately decide in advance, before an incident occurs, what that means. What is the minimum the system must still guarantee? Protecting user data from being accessed or corrupted is likely non-negotiable, everything else might be. We could decide to drop non-critical requests, use rate limiting or apply exponential back-offs, accepting that a legitimate user might be slowed down or temporarily blocked as a side effect.\nConsequence classes # Depending on the consequences a failure would have to a structure or its parts it is assigned a Consequence Class. This Consequence Class can then increase or decrease all loads in the persistent Design Situation (under normal use).\nIt also dictates how much supervision is needed during design (from self check to checking by another organization, the normal is checking by another person) and construction.\nWhich Consequence Class applies depends on multiple factors. One is of course the possible loss of human lives, but economical, social and environmental consequences weigh in as well. So the loss of faith of the public in an organization or the government can justify a high consequence class.\nIt seems intuitive to concentrate on the consequences of a failure and build in a buffer into systems whose failure would have extraordinary consequences. And on the other hand deliberately and consciously accepting lower robustness for lower-consequence systems. This way we know why we are chasing metrics (like Nines) and when we should put our efforts elsewhere. When thinking about consequences we should think about all negative consequences. It is obvious that safety-critical systems need to be designed with care, but economic and social consequences should be considered as well, even in Software Systems.\nRisk Strategy # Not every situation needs to be checked for every single project. A small house in a non-earthquake zone might not need to be designed for the Seismic Design Situation.\nPicking the right Design Situations is hard, especially for accidental situations. The engineer needs to balance the expected consequences of failures with the cost of the construction work.\nAnother strategy is to minimise the risk of the accident occurring in the first place, rather than designing to withstand it. Instead of increasing the resistance of the bridge to withstand a ship crashing into a pillar, the pillar can maybe be moved to the land? Or maybe the pillar can at least be protected in another way?\nChanging the strategy from designing for all possible accidents to risk avoidance applies to Software Engineering as well. At a smaller scale it could be something like implementing exponential back-offs, at a larger scale it might be using the principle of least privilege. Zooming out and trying to see a problem from another angle can be powerful in all (engineering) situations.\nThis is reflected in the risk mitigation strategies according to OWASP. For each risk you make an informed and documented decision about whether to accept, eliminate, mitigate or transfer the risk.\nDesign Simplicity # Risks can also be decreased and the durability of the structure increased if you make it easier for the people building the structure to work their best. That is something that was stressed a lot by my professors. If your design is complicated, people will have a hard time applying it. It is absolutely worth it to pick materials (like screws and reinforcement bars) in your design that are stronger than need be in order to keep the total number of different materials to pick from at a minimum. When I studied, it was even mandatory to work on site for 3 months, in order to get a feeling for how it felt to work in the environments I as a Structural Engineer would be creating.\nThis is a lesson I have learned first hand in Software Engineering. When systems are designed without the people who will implement them, the design is unlikely to survive contact with reality, because the code is the architecture. This leads to frustration and systems that can only be navigated by the few people who build it. The best way to find out if a design is too complex is to collaborate with the people who have to implement it. It is generally important to not overcomplicate things and sometimes it is even worth a slightly clumsier design if it makes it easier to understand. So choosing a monolithic architecture instead of microservices, or communicating via HTTP instead of using an event-driven architecture can be a humble move if that is what the team understands and believes in.\nConclusion # The process of Limit State Design is very structured. It forces the engineer to think about how to build reliable structures and what reliability means for the structure, before they start iterating and calculating. Although software is soft and therefore changeable I think a ground level of structure can help to build systems that are as robust, reliable, useful and maintainable as they need to be. By clarifying:\nWhat might threaten the stability and security of our system (Ultimate Limit States) What might threaten the usability of our system (Serviceability Limit States) How long does this system need to be alive (Design Working Life) Which extreme situations might occur, and are there other ways to prevent those (Design Situations \u0026amp; Reliability Management) What are the consequences of the system failing (economical, societal, human lives) (Consequence Classes) and balancing these points with economics we can create a foundation that helps us to decide what failure states to focus on when we are in the middle of an iteration.\n","date":"24 March 2026","externalUrl":null,"permalink":"/post/2026-03-limit-state-design/","section":"Posts","summary":"In the blogpost about structural idealization I explained how structural engineers model reality to answer the question whether their designed structure can withstand the forces applied to it. But how does an engineer know which forces will be applied to their structure? The weight of the structure can be approximated at best, and natural occurrences like wind or snow or a car crashing into the building are much harder to anticipate and quantify. And how does an engineer consider a car crashing into the structure while still building reasonably economical?\n","title":"Limit State Design","type":"post"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/tags/structural-engineering/","section":"Tags","summary":"","title":"Structural Engineering","type":"tags"},{"content":"I sometimes joke that I have gone full-circle: from Structural Engineer to Software Architect. Structural Engineering and Architecture are of course not at all the same thing (like you can never suggest that to either Structural Engineers or Architects!), but people tend to mix those two up. I in fact think it should be called \u0026lsquo;Software Structural Engineer\u0026rsquo; instead of Software Architect. That would be more correct and would make my joke funnier. Let me explain\u0026hellip;\nI have a Master of Science in Structural Engineering, focusing on geotechnical, concrete and steel structures but I became a Software Engineer as soon as I left university.\nDuring my studies I was always reminded (mostly by the professors) about this one fundamental truth: actio est reactio (action equals reaction). Structural Engineering is in its core the study of how forces travel through a system and how that system reacts to them. Reality is too complex to calculate all at once, so engineers use structural idealization, a concept I will explain in the following and relate how I think it applies to Software Engineering.\nTo determine if a system can safely transfer the forces applied to it to the ground, engineers use a model that consists of some very basic elements that are represented by some lines on a piece of paper. This is called the Global Analysis.\nModel used for the Global Analysis of a bridge (without loads) It lets them grasp the behavior of the entire system at a glance and answers the fundamental question of whether the structure as a whole will stand.\nThis comes at the cost of accuracy, because this analysis is simplified and assumes that every individual element will be able to withstand the forces applied to it. These assumptions need to be verified to find out if for example the concrete is strong enough, the soil can support the weight without yielding and the connections have enough screws and bolts.\nAnalyzing the parts # To verify the assumptions, elements are \u0026lsquo;cut-out\u0026rsquo; of the Global Analysis and the forces in the cuts are calculated (actio est reactio!). So the mental load of the system as a whole is replaced by forces at the boundaries (Interfaces!) of the new much smaller model. Which lets us add other details that we needed to neglect before, like the material used and the omitted dimensions of the element (the element was modelled as a two dimensional line before).\nKnowing where to place those cuts is a big part of the engineering training. You want to get away with as few subsystems as possible to keep the number of calculations down, so you need to find the most critical spots. In order to do that you look at different failure states. If the failure state you are looking at is bending for example, you cut your element free at the point where the bending forces in the material are likely highest. Learning where that spot likely is was a huge part of my training.\nWhen I first read about Domain-driven Design\u0026rsquo;s Bounded Contexts I was reminded of Structural Idealization. You have a Context Map where you make sure that your system is in balance, that it can take and distribute the load, but you are not interested in details here. And each Bounded Context is a free cut part of your system that communicates through its boundaries. The Bounded Context does not know about the system and no other system knows about the inner life of the Bounded Context, it does not leak its context and it solves its task. A Bounded Context is defined around a task (i.e. calculate if this part will be able to withstand these bending forces) and not around a subdomain (i.e \u0026lsquo;beams\u0026rsquo;, or \u0026lsquo;all elements that are made of steel\u0026rsquo;).\nEmbracing Iteration # Once the calculation is done the engineer might discover that in order to withstand the forces applied to it, the element needs to be stiffer or bigger and heavier than anticipated in the Global Analysis.\nThat, of course, triggers a recalculation of the Global Analysis and all other subsystems that were depending on the assumptions via the forces at their boundaries. And so every change in a subsystem triggers a recalculation of all other systems until the system is in equilibrium and we have made sure every element can bear its load in all relevant failure states.\nSoftware Professionals often think of physical construction as a one-shot solution. Unlike software, changes are hard and expensive in physical structures. We assume that because the result is hard to change retroactively, the design process must have been linear. Humans have been constructing buildings much longer than they have built software. We know where to cut, we have standardized models, and we have established safety factors and regulations (which is a topic for another post). Yet, we still iterate over a design multiple times.\nEven in standard systems, like a shallow foundation or a steel portal frame, the external factors like soil and water pressure or the specific wind loads, are never identical. A calculation fails at a specific joint, which forces a change in the overall thickness, which in turn forces a recalculation of the entire foundation. While the act of constructing a building is not iterative, the process of designing it is.\nDrawing parallels to Software Engineering is not hard here: iteration isn\u0026rsquo;t a failure in planning, it is inherent to designing complex systems. Instead of trying to think extra hard about problems beforehand we should embrace iterating over our design. It is a privilege and advantage that software can be changed so easily and cheaply.\nLast words # Not all problems can be solved by looking closer at the details. If a single element is failing because it is overloaded, adding more concrete to it often just makes the system heavier and more unbalanced. The same is true for adding more code to an overloaded module. Sometimes, you have to zoom out, look at the Global Analysis, and change the path the forces are taking entirely.\n","date":"19 February 2026","externalUrl":null,"permalink":"/post/2026-02-structural-idealization/","section":"Posts","summary":"I sometimes joke that I have gone full-circle: from Structural Engineer to Software Architect. Structural Engineering and Architecture are of course not at all the same thing (like you can never suggest that to either Structural Engineers or Architects!), but people tend to mix those two up. I in fact think it should be called ‘Software Structural Engineer’ instead of Software Architect. That would be more correct and would make my joke funnier. Let me explain…\n","title":"Structural Idealization","type":"post"},{"content":"I started my software development career as a structural engineer who programmed, developing analytical applications for other engineers. When I moved to a traditional software company, I went from one extreme to another, from having all the roles of a software development team at once to becoming a \u0026ldquo;code monkey\u0026rdquo; with no context about what I was building and why.\nIn search of a middle ground I came across Domain-Driven Design (DDD), which in its essence is about getting to know the Domain you are developing software for. But without being a Domain expert yourself.\nFor my next project I started my DDD journey by implementing what is known as tactical Domain-Driven Design. These are patterns applied at the code level. There is also Strategic Domain-Driven Design which describes patterns that can be applied on a higher, architectural level.\nI quickly started seeing advantages of applying tactical patterns that went beyond the separation of concerns, that an isolated Domain Layer with a model containing data and behavior (Rich Domain Model) brings. The pattern that stood out to me was Value Objects. It is relatively straightforward to grasp and implement and still allow you to practice the philosophy of DDD.\nIn this post I will show which features of the Kotlin Language I find especially useful to implement Value Objects (including Domain Primitives) while developing the SwatchIt Android application.\nWhat is a swatch? A swatch is a small sample of fabric knitted before starting a project. Because every knitter’s tension and materials vary, swatches allow you to measure your stitch and row count (gauge). This ensures your finished piece will be the correct size.\nAbout Value Objects # All data objects in a Domain-Driven Design Model are either Entities or Value Objects. Value Objects are objects without an identity that are primarily defined by their attributes. Entities are defined primarily by their identity, because we need to keep track of them over their whole lifecycle, even if all their attributes change. In the context of SwatchIt a swatch is an entity. The attributes, like the yarn or the notes, might be changed by the user, but we still see it as the same swatch. There could be two swatches with the same attributes in our system, but we see them as conceptually different swatches. So we cannot use the attributes to track the swatch. We need something that makes the swatch unique and identifiable in our system: an identity usually implemented as an Int, or a UUID.\nIn real life all physical objects have an identity. They might change, but we usually still see them as the same objects. The aim of modelling is to make things analyzable and calculable. To achieve that aim we usually have to strip everything from reality that is not relevant to our analysis.\nModelling means distilling reality into a concept that is exactly as complex as it absolutely needs to be for the task at hand. The model needs to strike a balance between being too naive to make any useful assumptions and being too complex to be analyzable and calculable. Distinguishing Value Objects and Entities is one tool to achieve that.\nIf you are not interested in following an object over its whole lifecycle, you model this object as a Value Object, without an identity. If the user changes the knitting needle size, it is not important which exact pair of knitting needles the user had in mind. All that is important in the context of SwatchIt is the size of the needles. The knitting needle object is defined by it\u0026rsquo;s attributes (the size) and not by an identity.\nThere are no general rules. What is considered a Value Object and what an Entity can change from context to context and every team decides for themselves what should be modelled as what.\nNot requiring an identity has several advantages:\nYou can share instances, which saves memory on the machine. Value Objects are immutable as per their definition. Meaning that an instance\u0026rsquo;s attributes are set during construction and cannot be changed after that. Leveraging immutability has several advantages:\nIt makes the code easier to reason about -\u0026gt; No need to take sideeffects into account that could occur because the instance changes. No defensive copying -\u0026gt; You can safely pass instances around and other objects can reference the same instance without risk that one object could change a shared instance. Better thread safety -\u0026gt; One thread cannot change an instance that another thread is using as well, if the instance is immutable. Testing is also more straight forward, because it is much easier to think of and cover all possible states. Implementation in Kotlin # When implementing Value Objects you can make use of features that your chosen language and framework offer you. Here comes an overview over what I found useful in Kotlin/JVM. Most of the examples are taken directly from the code of the SwatchIt Android application.\nData classes # When you compare Value Objects they are considered equal when they have the same attributes. You can use two knitting needle objects interchangeably as long as they have the same size value. To reflect that in the code you would override the Equals and HashCode methods to reflect that.\nInstead of doing that manually for each Value Object you can leverage Kotlin\u0026rsquo;s Data class. It automatically implements the Equals and HashCode methods based on the attributes of the class, which is exactly what we want.\nIt is also quite easy to create immutable data classes by defining all properties in the primary constructor as readonly with the val keyword:\ndata class Yarn( val name: YarnName?, val manufacturer: YarnManufacturer? ) If a property should not be considered for comparing it can be defined in the class body:\ndata class GaugeSize(val value: Double) { val unit: LengthUnit = LengthUnit.CM ... } // Equality will be determined by the value only, // the unit property is not taken into account Additionally data classes come with some useful standard methods implemented, for example a copy method that makes handling immutable objects easier and reduces boilerplate:\nval newYarn = oldYarn.copy(name = Name(\u0026#34;new yarn name\u0026#34;)) // creates a new yarn instance with the same manufacturer value // as oldYarn, but a different name The automatic toString method of a Yarn instance generates: \u0026quot;Yarn(name=\u0026quot;new yarn name\u0026quot;, manufacturer = \u0026quot;manufacturer\u0026quot;)\u0026quot;, which is helpful for debugging.\nAs someone being familiar with C# I see resemblances between Records in C# and data classes in Kotlin. One difference I noticed is that you cannot have a private constructor in data classes. So forcing the creation of instances via a factory method is not possible. However you must use the Primary Constructor. Validation can be added to an init block like this:\ndata class Yarn( val name: YarnName?, val manufacturer: YarnManufacturer? ) { init { require(name != null || manufacturer != null) { \u0026#34;Yarn must have at least a name or manufacturer\u0026#34; } } ... } The init block is run when the Primary Constructor is executed. This behavior makes sure that the validation logic in the init block is always run, even if an object is created via the copy method. This can be somewhat of a pitfall in C#, unless you are careful, the with expression (which is C# equivalent to copy) can bypass constructor-based validation, leading to invalid domain states. Also with clones the whole object and then sets the properties one by one. The init block makes validation of Cross-Field Invariants easier, as it ensures the object is only fully initialized if the entire state passes validation in one atomic operation.\nAnother difference is that Kotlin\u0026rsquo;s data classes implement structural equality by default even for collection properties (be careful with Arrays though).\ndata class Yarn(val skeins: List\u0026lt;Skein\u0026gt;) data class Skein(val yardage: Long, val color: String) class Swatch(val skeins: List\u0026lt;Skein\u0026gt;) fun main() { val blueSkein = Skein(200, \u0026#34;blue\u0026#34;) val redSkein = Skein(220, \u0026#34;red\u0026#34;) val skeins1 = listOf(blueSkein, redSkein) val skeins2 = listOf(blueSkein, redSkein) // structural equality for collections println(skeins1 == skeins2) // true val yarn1 = Yarn(listOf(blueSkein, redSkein)) val yarn2 = Yarn(listOf(blueSkein, redSkein)) // structural equality in data classes println(yarn1 == yarn2) // true val swatch1 = Swatch(listOf(blueSkein, redSkein)) val swatch2 = Swatch(listOf(blueSkein, redSkein)) // referential equality in classes println(swatch1 == swatch2) // false } Furthermore, it is possible to inherit from Records in C#. Records use an EqualityContract property to make sure that you are comparing the same type (and not a parent with a child for example). That makes overriding the Equals and HashCode methods of records a little tricky as you need to remember to add a check EqualityContract == other.EqualityContract to maintain type-safety. So not only do you have to override those methods as soon as you use a collection property, you also have to remember the EqualityContract.\nIn Kotlin all classes are by default closed for inheritance (final). To open a class for inheritance you need to add the open keyword. But Data classes can never be open. If you need inheritance you typically use a sealed interface or a regular abstract class instead.\nInterfaces in Kotlin can be sealed, which means that only classes within the same module can inherit from the interface. All the inheritors are therefore known at compile-time. This is nice because it gives you full control over your interfaces in the Domain Layer and makes it clear that this interface should only be extended within the context of this module.\nAs an example SwatchIt uses a sealed interface to distinguish between two types of measurements (rows and stitches) like this:\nsealed interface Measurement { val count: GaugeCount val size: GaugeSize data class Rows( override val count: GaugeCount, override val size: GaugeSize ) : Measurement data class Stitches( override val count: GaugeCount, override val size: GaugeSize ) : Measurement } As all possible inheritors are known at compile time, there\u0026rsquo;s no need to add a default value in when expression blocks, when checking the type of the Measurement instance, because the compiler enforces exhaustiveness:\nprivate fun Measurement.toListItem(): MeasurementListItem = when (this) { is Measurement.Stitches -\u0026gt; MeasurementListItem.Stitches( count = count.value, size = size.value ) is Measurement.Rows -\u0026gt; MeasurementListItem.Rows( count = count.value, size = size.value ) } Again: less boilerplate, yay!\nInline value classes # Inline value classes are meant for wrapping underlying types (like Long, Int, String..). They give you the possibility to use more domain specific types without increasing the runtime overhead (in most cases). This is perfect for wrapping these simple values into small Value Objects (also called Domain Primitives). You get type safety, immutability and validation without any performance overhead.\nInline value classes are defined by the keyword value, and they need the @JvmInline attribute if your code is supposed to be run on the JVM:\n@JvmInline value class YarnName private constructor(val value: String) To ensure they can be \u0026ldquo;unwrapped\u0026rdquo; into a single value, these classes are limited. They must have exactly one primary constructor parameter and no other backing fields. However, you can still add computed properties and member functions. These are compiled to static Java methods, meaning you can add logic to your domain primitives without breaking the performance optimization.\nYou might want to override the toString() method of your inline value classes. By default, the Kotlin compiler generates a toString() that includes the class name, like YarnName(value=\u0026quot;My Yarn Name\u0026quot;). If you want your domain primitive to behave exactly like its underlying type when printed (e.g., just printing My Yarn Name), you should override it:\noverride fun toString(): String = value Looking at the decompiled bytecode, we can see that this override doesn\u0026rsquo;t \u0026ldquo;break\u0026rdquo; the optimization. The compiler simply replaces its generated static method with our custom static method:\n@NotNull public static String toString_impl/* $FF was: toString-impl*/(String var0) { return var0; } As with data classes, Equals and HashCode methods do not need to be overwritten, they use the underlying type for comparison.\nUnlike in data classes, you can use private primary constructors in value classes and thus force the creation via factory methods in Companion Objects:\n@JvmInline value class YarnName private constructor(val value: String) { init { require(validate(value) is ValidationResult.Success) { \u0026#34;YarnName invalid\u0026#34; } } companion object { private const val MAX = 50 fun validate(value: String): ValidationResult { return when { value.length \u0026gt; MAX -\u0026gt; ValidationResult.Error(\u0026#34;max 50\u0026#34;) else -\u0026gt; ValidationResult.Success } } fun create(value: String): YarnName? = if (value.isBlank()) null else YarnName(value) } override fun toString(): String = value } I\u0026rsquo;ll get back to the ValidationResult interface in a minute.\nUsing factory methods to create objects is another widely used concept in Domain-Driven Design. Sometimes the creation of an object is complicated and you want this logic to stay in within the class. You just expose a convenient creation method (aka factory method) that speaks its intent. My example is, as you can see, pretty minimal, but I hope you get the concept.\nCompanion Objects # In Kotlin, class level functions and properties have to be implemented within so called companion objects. The functions and properties within the companion objects can be called the same way static functions and properties are called in other languages. I just wanted to mention them here, because it took me while to get used to them. And as you need static functions to implement factory methods and static validation here is an example of how you could implement both with a Companion object:\ncompanion object { private const val MAX = 50 fun validate(value: String): ValidationResult { return when { value.length \u0026gt; MAX -\u0026gt; ValidationResult.Error(\u0026#34;max 50\u0026#34;) else -\u0026gt; ValidationResult.Success } } fun create(value: String): YarnName? = if (value.isBlank()) null else YarnName(value) } The ValidationResult implementation looks like this (note the sealed interface in action again):\nsealed interface ValidationResult { data object Success : ValidationResult data class Error( val errorMessageId: Int ) : ValidationResult } It wraps the result of a validation, so instead of just returning a bool, it can return an error message id, that can be used to show the specific validation error to the user.\nAnother option is to throw an exception. A failing validation is in this case expected, as the method is used to validate user input.\nIf however the application tries to create an instance that is not valid, that would be unexpected, because at this point the input should have been validated. Something must have gone wrong and the application throws an exception.\nThis has no direct connection to Domain-Driven Design though, it is just how I like to handle validation at the moment. However validation plays a big role in Domain-Driven Design.\nValidation # Making sure your application never ends up in an invalid state is the responsibility of the domain layer. Knowing what constitutes a valid or an invalid state is domain knowledge. Writing all those checks produces lots and lots of code and can be tedious. Kotlin is here to help!\nThe build-in require function shrinks validation code to a minimum:\nrequire(validate(value).isValid) { \u0026#34;Pattern invalid\u0026#34; } It throws an InvalidArgumentException, which is pretty generic. You might want to throw custom exceptions for you and others using your code to be able to react to them in a more fine grained manner.\nSimilar to require there is also the check function which works in the same way as the require function but throws an IllegalStateException.\nExtension methods # Value Objects often need small utility functions for formatting, conversion, or calculation. Rather than adding every helper function as a class member, Kotlin\u0026rsquo;s extension methods let you keep related functionality close to where it\u0026rsquo;s used while maintaining clean, readable Value Object definitions.\nKotlin does not require you to wrap functions in classes, they can be declared at the top level of a file. That reduces the boilerplate code otherwise needed to write extension methods. Here\u0026rsquo;s an example of an extension method:\nprivate fun Double.roundToOneDecimalPlace(): Double { val multiplier = 10.0 return round(this * multiplier) / multiplier } this references the value that the function is called upon in this case.\nClasses defined within the same file as the function can then call that function (it is marked private):\nfun calculateWidthFor(stitches: Count): Size { val result = (stitches.value * size.value / nrOfStitches.value) .roundToOneDecimalPlace() return Size(result) } This makes it clear that this extension function is only valid within the file/class/package it is placed in and it is better discoverable by The IDE than passing the double value as a parameter.\nOperator overloading # Operator overloading can be convenient for Value Objects, especially for Domain Primitives that only wrap a primitive type. Instead of for example adding two Counts by calling their values (count1.value + count2.value) the + operator can be overloaded like this:\n@JvmInline value class Count(val value: Long) { operator fun plus(summand: Count): Count { return Count(value + summand.value) } } // Tests: @Test fun add_validSummands_returnsSumOfValues() { val sum = Count(13) + Count(14) assertEquals(Count(27), sum) } Operator overloading should of course not be overused or misused. It must be obvious for the developer calling the overload what should happen.\nConclusion # And that concludes my list of suggestions. If you haven\u0026rsquo;t tried Kotlin I can recommend the Playground where you can run code in your browser. From the books on Kotlin I have read so far I liked Kotlin in Action best.\nI found Kotlin to be very convenient to implement Value Objects. Implementing them is boilerplate-prone and can seem as over-engineering, especially for people who haven\u0026rsquo;t seen the advantages yet. Kotlin does a lot of the heavy lifting and invalidates those arguments, it lets you write concise code while giving you the advantages of leveraging Value Objects.\nIf you have remarks, ideas I should consider or just feel like discussing, please reach out. I am by no means set in my ways and super interested in your experiences and opinions.\n","date":"6 January 2026","externalUrl":null,"permalink":"/post/2026-01-06-value-objects-in-kotlin/","section":"Posts","summary":"I started my software development career as a structural engineer who programmed, developing analytical applications for other engineers. When I moved to a traditional software company, I went from one extreme to another, from having all the roles of a software development team at once to becoming a “code monkey” with no context about what I was building and why.\nIn search of a middle ground I came across Domain-Driven Design (DDD), which in its essence is about getting to know the Domain you are developing software for. But without being a Domain expert yourself.\n","title":"Implementing Value Objects in Kotlin","type":"post"},{"content":"","date":"6 January 2026","externalUrl":null,"permalink":"/tags/kotlin/","section":"Tags","summary":"","title":"Kotlin","type":"tags"},{"content":"","date":"6 January 2026","externalUrl":null,"permalink":"/tags/value-objects/","section":"Tags","summary":"","title":"Value Objects","type":"tags"},{"content":"About a year ago I attended a workshop held by Dan Bergh Johnsson and Daniel Deogun, authors of the book Secure By Design. They presented the topics of their book and on three additional occasions we met in smaller groups and had guided discussions on the topics.\nI like the mix of Domain Driven Design and cybersecurity a lot, it gave me a whole new perspective (and justification!) on why to apply Domain Driven Design. One concept I have been using extensively since I read the book is Domain Primitives and I want to share how I utilize it.\nDomain Primitives are described as the smallest building blocks in a domain model. A Domain Primitive is a small Value Object, a type that is primarily defined by its attributes rather than by an identity.\nAccording to the authors nothing in a domain model should be represented by a primitive type (like int, string, double..). Everywhere you\u0026rsquo;d use a primitive type you replace it with a Domain Primitive. The primitive type then becomes the Domain Primitive\u0026rsquo;s only attribute.\nConceptually a Domain Primitive is a Value Object. The name Domain Primitive just makes the intention clear: to replace primitive types with Value Objects. I have found that having a name for this concept makes it easier to apply it consistently and to communicate the pattern to team members.\nThe first time I got the chance to apply this concept, I was working on an ASP.NET Core Web API that was meant to track projects through their lifecycle following a rather complicated process. The Project Entity might have looked something like this (simplified and generalized):\npublic class Project { public int Id { get; } public ProjectStatus ProjectStatus { get;} public string Name { get; } public string Description { get; } public Project(int id, ProjectStatus projectStatus, string name, string description) { Id = id \u0026lt; 0 ? throw new ArgumentOutOfRangeException(nameof(id)) : id; ProjectStatus = projectStatus; Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException(\u0026#34;Name cannot be empty\u0026#34;) : name; Description = description; } } Turning all the primitive types into Domain Primitives the code becomes:\npublic class Project(ProjectId id, ProjectStatus projectStatus, ProjectName name, Description description) { public ProjectId Id { get;} = id; public ProjectStatus ProjectStatus { get;} = projectStatus; public ProjectName Name { get; } = name; public Description Description { get;} = description; } I would argue that the code is better readable, especially for people who don\u0026rsquo;t read code every day. Someone instantiating this class will no longer accidentally switch the name and the description, the Domain Primitives have introduced type safety to the constructor.\nNotice how the validation for the Id and Name is gone. It was moved into the Domain Primitive. A Domain Primitive is responsible for its own validation. The validation should be done directly in the constructor, so that it is impossible to create an instance that is in an invalid state.\nLet\u0026rsquo;s look at the ProjectName Domain Primitive (it\u0026rsquo;s the same concept for the Description). In its simplest form the code looks like this:\npublic record ProjectName { public string Value { get; } public ProjectName(string value) { Value = string.IsNullOrWhiteSpace(value) ? throw new ArgumentException(\u0026#34;Name cannot be empty\u0026#34;) : value; } } I really like that the file is so small now and that I can concentrate on it without being distracted by the surrounding code. Because of that I started thinking about the validation a lot more. There is probably a sensible maximum length for a ProjectName. And maybe we should not allow all characters? Setting sensible limits can contribute to defense against injection attacks (see OWASP A03 Injection), as the characters needed for such an attack are rejected by the validation.\nAfter adding the missing validation the ProjectName now looks something like this:\npublic record ProjectName { public string Value { get; } public ProjectName(string value) { var validationResult = Validate(value); if (validationResult is ValidationResult.Failure failure) throw new DomainValidationException(failure.Error); Value = value; } public static ValidationResult Validate(string value) { if (string.IsNullOrWhiteSpace(value)) return new ValidationResult.Failure(\u0026#34;Name cannot be empty\u0026#34;); if (value.Length \u0026gt; 100) return new ValidationResult.Failure(\u0026#34;Name cannot exceed 100 characters\u0026#34;); if (!IsValidCharacters(value)) return new ValidationResult.Failure(\u0026#34;Invalid characters in Name, only letters, number and whitespace allowed\u0026#34;); return new ValidationResult.Success(); } private static bool IsValidCharacters(string value) =\u0026gt; value.All(c =\u0026gt; char.IsLetterOrDigit(c) || char.IsWhiteSpace(c)); } The ValidationResult type looks like this:\npublic abstract record ValidationResult { public record Success : ValidationResult; public record Failure(string Error) : ValidationResult; } The validation logic was extracted into a separate static method, so there is a way to shut the program down more gracefully, but also to reuse it and test it independently.\nNote that you want to validate as soon as possible to stop malicious input, which means that syntactic validation should be done before any other of your code is executed.\nIn ASP.Net Core projects, I usually validate the Request Models with Attribute Validation and validate against the same rules as in the Domain Primitives. This leads to duplication, but is straight forward, easy to use and readable. The validation will also be shown in OpenAPI, which means that the frontend can reuse it.\nIn Android Compose, I reuse the validation for input validation. As soon as the user changes an input field, I validate it with the static validation method in the Domain Primitive.\nThe validation in the Domain Primitive serves as a safety net and canonical validation, but it is not a replacement for early syntactic input validation.\nYou generally always start with syntactic validations (format, length, allowed characters), then do semantic validations (business rules that require additional context or data to be loaded), as semantic validations can themselves be a cause of security issues. For example, if you need to check whether a username already exists in the database, performing this check on unsanitized input could expose you to SQL injection attacks.\nSemantic checks that can be done in a Domain Primitive are usually static validations, like the IsValidCharacters check in the code above. If it is possible to solve the validation in another way, I avoid using RegularExpressions as they can be problematic.\nWhen an invalid input reaches the constructor of a Domain Primitive it should already be valid, as it was possible to validate it earlier. So something has seriously gone wrong (or even tampered with) and I throw an exception (and log it) instead of for example returning a Result Wrapper.\nKnown advantages # The advantages that are listed in the book are that Domain Primitives increase security by immutability, failing fast and validation which makes it impossible to get to an invalid state. Domain Primitives force us to think more about validation. Other advantages I see are stronger type safety, better readability (in the classes using the Domain Primitives) and testability. It is also much easier to keep all the validation in one place which reduces defensive coding, if you have an instance of a Domain Primitive you know that you don\u0026rsquo;t have to check if it is valid.\nHidden advantages # Apart from those advantages I noticed another hidden advantage of using Domain Primitives instead of Primitive types. The code has more meaning, the ubiquitous language comes more alive in the code. And because I could not know what makes a ProjectName syntactically and semantically correct, I asked the other stakeholders and domain experts. I thought I would get a quick answer, but the question actually led to some discussions and more people were asked. It turned out that not only were there rules on the validation but the ProjectName was some kind of identifier for a Project, there were rules for duplicate names that were dependent on the state the Project was in. Rules that were completely implicit. Eventually, we ended up writing Domain Service logic.\nUsing Domain Primitives made me really think about the small concepts in the models I was working with, and that makes it easier to start asking questions. In the above example the team discovered the rules around ProjectNames. I noticed this pattern several times when I transformed primitive types to ValueObjects. Once I made the type explicit I started thinking about its semantics a lot more and could make informed decisions about what was important and what could be neglected. This was especially remarkable when I transformed a Swedish Social Security number (Personnummer) from a String to a ValueObject. There is a lot of logic in these numbers, so much that a Domain Primitive wasn\u0026rsquo;t enough. If you are interested here is some code on GitHub you can check out. Even a simple telephone number contains a lot of information if you start dissecting it.\nDomain Primitives are easy to implement even in legacy systems and big balls of mud. I found them to be a powerful tool to start applying Domain Driven Design concepts, to start asking meaningful questions and making implicit concepts explicit, all while increasing the safety of the codebase. So next time you come across a primitive type in your business logic I hope you give them a try!\n","date":"10 November 2025","externalUrl":null,"permalink":"/post/2025-11-10-domain-primitives/","section":"Posts","summary":"About a year ago I attended a workshop held by Dan Bergh Johnsson and Daniel Deogun, authors of the book Secure By Design. They presented the topics of their book and on three additional occasions we met in smaller groups and had guided discussions on the topics.\nI like the mix of Domain Driven Design and cybersecurity a lot, it gave me a whole new perspective (and justification!) on why to apply Domain Driven Design. One concept I have been using extensively since I read the book is Domain Primitives and I want to share how I utilize it.\n","title":"My Experiences with Domain Primitives","type":"post"},{"content":"","date":"27 October 2025","externalUrl":null,"permalink":"/tags/android/","section":"Tags","summary":"","title":"Android","type":"tags"},{"content":"","date":"27 October 2025","externalUrl":null,"permalink":"/tags/fdroid/","section":"Tags","summary":"","title":"Fdroid","type":"tags"},{"content":"","date":"27 October 2025","externalUrl":null,"permalink":"/tags/instruction/","section":"Tags","summary":"","title":"Instruction","type":"tags"},{"content":"I published SwatchIt, a native Android app I have been working on, to the F-Droid Android Store 🎉. Now you can get it on F-Droid.\nAll apps published through F-Droid are free and open source software (FOSS) and F-Droid itself is free and open source as well.\nIf you don\u0026rsquo;t already have F-Droid installed, you should go ahead and install it, it is not as complicated as it looks even on certified Android devices (for now.. but that is a topic for another post).\nPublishing to F-Droid comes with some efforts, mainly since every app that you download through F-Droid must be built by F-Droid as well, so it is not enough to upload an APK.\nBelow I outline the steps that I needed to take in order to get the app published. I structured them into things that needed to be done, things that are nice to have and problems I ran into.\nYou can find the source code to my app SwatchIt on Codeberg.\nThings I needed to do # Before you start # The app I have published is a Native Android App that I developed with Android Studio. I am running Ubuntu 24.04 on my local machine. As already mentioned the app you want to publish must be Open Source, so the source code must be publicly available in a version control repository (supported repository types).\nThe official Submitting to F-Droid Quick Start Guide is a great starting point and source of information.\nLicensing # Every FOSS application also needs Licensing information. F-Droid expects a FOSS license file with the Name LICENSE in the root directory of your repository. I consulted the Wikipedia overview of FOSS licenses and picked GNU GPL 3.0 or later. I copied the license text into the LICENSE file.\nI got a comment on the Merge Request to add copyright information as well. The License suggests to add copyright and license information to every source file. I added the following block to the README instead:\nCopyright (C) 2025 Katharina Damschen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see \u0026lt;https://www.gnu.org/licenses/\u0026gt;. That was accepted as sufficient.\nMetadata # The F-Droid store creates a page for each app showing informational texts, screenshots, changelogs and other metadata to the user. This is why you need to add this metadata to your repository.\nF-Droid supports two different ways to structure your metadata: Triple-T and Fastlane, both are described in the Metadata guidelines.\nI went with the Fastlane structure, because that is how it is done in the F-Droid client repo. I placed the files under metadata/en-US,en-US is the default and fallback value and you need to provide this folder.\nYou can see my files and structure in my repository on Codeberg.\nApp Version # Applications need to be versioned in order to be able to update them. Decide on a versioning scheme and add the versionName as well as the versionCode to your project\u0026rsquo;s build.gradle file. You can read about how to do that in the Android docs.\nI use semantic versioning as my versioning scheme. As this was the first version and the app is not stable yet I chose 0.1.0 as the first versionName and 1 as the versionCode. Here is what the defaultConfig block in my project\u0026rsquo;s build.gradle file looks like:\nandroid{ defaultConfig { applicationId = \u0026#34;net.damschen.swatchit\u0026#34; minSdk = 21 targetSdk = 35 versionCode = 1 versionName = \u0026#34;0.1.0\u0026#34; } } Tag and push # All the necessary code changes in your app have been done now! Commit and tag the commit with v%versionName. So in my case I tagged with v0.1.0.\nMake sure to push the commit and the tag (git push --tags).\nCreate a reproducible build # Enabling reproducible builds is encouraged by F-Droid, especially for new apps. You build and sign your APK with your own key, F-Droid builds the same app unsigned, copies your signature to their build, and if it verifies correctly (proving identical binaries), they publish your originally-signed APK, making installations independent of F-Droid.\nYou can also chose to let F-Droid sign your APK, but the problem is that a device will not recognize the same app signed with different keys as the same app. If you decide to use your own signing key later, the users will have to uninstall and reinstall your app with the new signing key. That is why reproducible builds are encouraged, so you will be able to distribute your app via different channels, independent of F-Droid\u0026rsquo;s signing key\nCreating two binaries that are exactly the same is not that easy. It\u0026rsquo;s easier the more your build process resembles F-Droids build process, but more valuable the more the processes differ.\nFirst you need to create a signing key that you store in a keystore. I created a new keystore along with a signing key in a subdirectory of my home directory (~/.keystore/) and made the key valid for 1000 years:\nkeytool -genkey -v -keystore YOUR_KEYSTORE_PATH -alias YOUR_KEY_ALIAS -keyalg RSA -keysize 2048 -validity 365000 I didn\u0026rsquo;t find much information about which signing algorithms are supported, both the Android and F-Droid Documentation use RSA, so I used it as well.\nThe Android Documentation also mentions that \u0026ldquo;Your key should be valid for at least 25 years, so you can sign app updates with the same key through the lifespan of your app.\u0026rdquo;\nYou should choose a long validity for the key, because Android won\u0026rsquo;t accept your app as the same when signed with a new key.\nWhen I had the key I built the APK via the CLI (make sure that you are building the tagged version of the code. You cannot have any uncommitted changes or the build will not be reproducible):\n./gradlew assembleRelease This creates an unsigned APK in app/build/outputs/apk/release. I renamed the APK to swatchit-0.1.0.apk:\nmv app/build/outputs/apk/release/app-release-unsigned.apk YOUR_APK_PATH/swatchit-%versionName.apk and then signed it with the help of the apksigner tool:\napksigner sign --ks-key-alias YOUR_KEY_ALIAS --ks YOUR_KEYSTORE_PATH YOUR_APK_PATH/swatchit-%versionName.apk Now you need to publish your signed APK somewhere were F-Droid can access it.\nI created a release on Codeberg and uploaded the signed APK manually. I haven\u0026rsquo;t migrated any pipelines to Codeberg yet, but ideally this process would be automated.\nAdd a manifest to the fdroiddata repository # Now that you have prepared your app, you need to inform F-Droid about its existence. You do that by adding a metadata file to the fdroiddata repository. Create a GitLab account or use your existing one and fork the fdroiddata repository.\nDownload the repository:\ngit clone https://gitlab.com/YOUR_FORKS_ADRESS switch to the root folder and create a branch named after your applicationId:\ncd fdroiddata git switch -c your.app.id Copy the manifest template file, make sure the file ending is .yml:\ncp templates/build-gradle.yml metadata/your.app.id.yml The values that can be used in the manifest file are documented in F-Droid\u0026rsquo;s Metadata Reference.\nI had a hard time picking a category and went with Internet, and it got changed to Sports \u0026amp; Health by the F-Droid team. F-Droid has actively decided to keep the number of Categories small, the downside to this is that the category for your app might not fit very well.\nHere is the file:\nCategories: - Sports \u0026amp; Health License: GPL-3.0-or-later SourceCode: https://codeberg.org/katharinad/SwatchIt IssueTracker: https://codeberg.org/katharinad/SwatchIt AutoName: SwatchIt RepoType: git Repo: https://codeberg.org/katharinad/SwatchIt Binaries: https://codeberg.org/katharinad/SwatchIt/releases/download/v%v/swatchit-%v.apk Builds: - versionName: 0.1.0 versionCode: 1 commit: v0.1.0 subdir: app gradle: - yes AllowedAPKSigningKeys: 785ce16a7ed68cdf6bd5a0181d0fea6fdf7b8a3813fd573b3748747740583351 AutoUpdateMode: Version UpdateCheckMode: Tags CurrentVersion: 0.1.0 CurrentVersionCode: 1 The template does not contain any optional properties, so I added those that I needed for reproducible builds, automatic updates and convenience.\nTo enable reproducible builds I had to add those two lines:\nBinaries: https://codeberg.org/katharinad/SwatchIt/releases/download/v%v/swatchit-%v.apk AllowedAPKSigningKeys: 785ce16a7ed68cdf6bd5a0181d0fea6fdf7b8a3813fd573b3748747740583351 The Binaries path is the path to the release, with %v getting automatically replaced with the versionName. I added my public signing key to AllowedAPKSigningKeys, after extracting it with this command:\napksigner verify --print-certs YOUR_APK_PATH | grep SHA-256 I also enabled the AutoUpdateMode by Version. As I understand it there\u0026rsquo;s a daily job running looking for updates on all apps. So all I have to do in the future is to push a new tag and create a new release with the name defined in the Binaries path to publish a new version.\nCommit and create a Merge Request # When you are done with filling in the metadata commit your changes to your fork with the message: \u0026lsquo;New App: your.app.id\u0026rsquo;. There is a Gitlab CI pipeline that checks if F-Droid can build your app and if it can reproduce it along with linting checks. If the pipeline passes create a Merge Request and wait ⏳!\nOnce your config file is merged it will take a few days until the app shows up in F-Droid. I got this link with additional information after the merge.\nYou can also take a look at my Merge Request. It took about 3 weeks until the code was being reviewed and 3 days after the merge the app was downloadable in F-Droid! It was even shown on the homescreen in the \u0026lsquo;Latest\u0026rsquo; tab!\nNice to Have # Here are some things that I did that weren\u0026rsquo;t totally necessary but nice to have or know about!\nBuild locally with the fdroidserver image # The official Submitting to F-Droid Quick Start Guide makes it sound like you have to check if you can build your app locally in a container running fdroidserver, but all the necessary checks are also being run by the fdroiddata CI pipeline.\nRunning the tasks locally is faster and the linting task fixes your manifest file locally, so I personally would use the Image again next time. It is also kind to not overuse F-Droids CI server.\nIf you want to have a go, here are the steps I needed to take to get fdroidserver running locally:\nClone the repo:\ngit clone https://gitlab.com/fdroid/fdroidserver Install docker, if you haven\u0026rsquo;t already.\nStart the container and map your fdroiddata and fdroidserver repo folders into the container:\nsudo docker run --rm -itu vagrant --entrypoint /bin/bash \\ -v FULL_PATH_TO_FDROIDDATA:/build:z \\ -v FULL_PATH_TO_FDROIDSERVER:/home/vagrant/fdroidserver:Z \\ registry.gitlab.com/fdroid/fdroidserver:buildserver Note that the paths need to be full paths, or you will get permission errors.\nThen in the container (following the steps in the guide):\n. /etc/profile export PATH=\u0026#34;$fdroidserver:$PATH\u0026#34; PYTHONPATH=\u0026#34;$fdroidserver\u0026#34; export JAVA_HOME=$(java -XshowSettings:properties -version 2\u0026gt;\u0026amp;1 \u0026gt; /dev/null | grep \u0026#39;java.home\u0026#39; | awk -F\u0026#39;=\u0026#39; \u0026#39;{print $2}\u0026#39; | tr -d \u0026#39; \u0026#39;) cd /build fdroid readmeta fdroid rewritemeta your.app.id Now you need to set the serverwebroot environment variable, otherwise you\u0026rsquo;ll see an error (though the actual tasks might still succeed). This is how it is done in the fdroiddata CI pipeline but not mentioned in the guide:\nexport serverwebroot=/tmp I then got:\nrsync: [Receiver] mkdir \u0026#34;/tmp/repo/status\u0026#34; failed: No such file or directory So I created the repo directory:\nmkdir /tmp/repo Then I could resume with the commands in the guide:\nfdroid checkupdates --allow-dirty your.app.id fdroid lint your.app.id fdroid build your.app.id Now you can fix any errors you got, or go on and push your changes and create a Merge Request.\nBuild and sign the APK with Android Studio # You can use Android Studio instead of doing the signing manually as I described above. Android Studio can create a signed APK for you, it can even create a keystore and a signing key.\nTo avoid the binary representation of build dependencies to be created and signed with a public Google Key when building with AndroidStudio or Intellij you need to add this block to your projects build.gradle file:\nandroid { dependenciesInfo { includeInApk = false includeInBundle = false } } I saw that this was commented on in some Merge Requests with links to two issues on GitLab (one and two).\nYou can start your build in Android Studio by clicking Build -\u0026gt; Generate Signed App Bundle or APK then choose the APK-option to obtain a signed APK.\nThen you can go on and upload the APK as a release and continue with adding a manifest to the fdroiddata repo\nThings that went wrong # Here are some problems I ran into and explanations on how I fixed those.\nDependency verification failed for lint task # You won\u0026rsquo;t run into this if you don\u0026rsquo;t use Dependecy verification.\nThe fdroid build task failed, because the gradle lint task (:app:lintVitalAnalyzeRelease) required some dependencies that were not yet listed in my verification-metadata.xml file. I got the same problem locally when running ./gradlew lint. To fix this, I reran the bootstrapping with the lint task:\n./gradlew --write-verification-metadata pgp,sha256 --export-keys lint I found information about this behavior in the gradle documentation.\nThis problem was not F-Droid specific, I just had never run the lint task before and wasn\u0026rsquo;t aware that it might need different dependencies, so it took my a while to understand the issue. If you know, you know!\nBuild not reproducible # After I activated reproducible builds the build failed, because it was not reproducible 🙃.\nF-droid lists common causes for unreproducible builds and suggests fixes.\nThere are ways of diffing the APKs described there too. I started with the most common and easiest measures without diffing: I made sure that I tagged before I build and that there were no uncommitted changes. I also build the APK and signed it with the command line instead of with Android Studio.\nThe next time I tried the build was reproducible, but I cannot make a statement on what it was of these three things that fixed the problem.\nI left the dependeciesInfo block that I added when I built and signed with Android Studio, although technically it is not needed when building releases with gradle via the CLI.\nIn the Merge Request the building bot left an annotation that there is a version mismatch between the gradle version and the gradle wrapper version in my project, which might explain the differences as well.\nF-Droid Build server does not support AGP \u0026gt;= 8.12.x # You won\u0026rsquo;t run into this if yu use AGP version 8.11 or lower\nThe annotation of the bot, that my gradle and gradle wrapper versions mismatched triggered an immediate urge in me to just update EVERYTHING! I hadn\u0026rsquo;t done that in a while because I wanted to focus on getting the app released. But now I had finally created a Merge Request. What could possibly go wrong?\nSo I updated the gradle wrapper and gradle and while I was at it, I updated the Android Gradle Plugin (AGP) to version 8.14.3.\nThe reviewer of the Merge Request then commented that I will need to downgrade AGP to the latest 8.11 version, because right now the build server hardware cannot handle higher AGP versions. You can find the issue on GitLab. So I downgraded to 8.11.3. I kept the new gradle and gradle wrapper versions, though.\nF-Droid Build Container and Server do not have Java 21 available # You won\u0026rsquo;t run into this if you use a Java version 17\nI had hoped my Merge Request would get approved without me having to create a new release. But the APG version and the missing Copyright notice made the reviewer ask for a new release.\nAt least I could benefit from this documentation immediately 🙃! When I tried building with F-Droid locally in the container I got a new (to me) error:\nFAILURE: Build failed with an exception. * What went wrong: Execution failed for task \u0026#39;:app:compileReleaseJavaWithJavac\u0026#39;. \u0026gt; Java compilation initialization error error: invalid source release: 21 Of course I had also updated the Java version from 17 to 21. I updated everything! And the container (as well as the CI Server) does not support that yet.\nOthers fortunately had the same problem and one grep later I had found their solution: I needed to add some sudo commands for installing Java 21 before building, and making it available for the build to the configuration file in fdroiddata.\nHere are the updated parts:\nBuilds: - versionName: 0.2.0 versionCode: 2 commit: v0.2.0 subdir: app sudo: - echo \u0026#34;deb https://deb.debian.org/debian trixie main\u0026#34; \u0026gt; /etc/apt/sources.list.d/trixie.list - apt-get update - apt-get install -y -t trixie openjdk-21-jdk-headless - update-alternatives --auto java gradle: - yes AllowedAPKSigningKeys: 785ce16a7ed68cdf6bd5a0181d0fea6fdf7b8a3813fd573b3748747740583351 AutoUpdateMode: Version UpdateCheckMode: Tags CurrentVersion: 0.2.0 CurrentVersionCode: 2 Note, however, that this did not work in the container running locally on my machine. It ignores the sudo commands:\nWARNING: net.damschen.swatchit:0.2.0 runs this on the buildserver with sudo: [\u0026#39;echo \u0026#34;deb https://deb.debian.org/debian trixie main\u0026#34; \u0026gt; /etc/apt/sources.list.d/trixie.list\u0026#39;, \u0026#39;apt-get update\u0026#39;, \u0026#39;apt-get install -y -t trixie openjdk-21-jdk-headless\u0026#39;, \u0026#39;update-alternatives --auto java\u0026#39;] These commands were skipped because fdroid build is not running on a dedicated build server. So I pushed and checked the build pipeline and the build succeeded. 🎉\nConclusion # This post got longer than expected because I describe the details and caveats I ran into, but I hope this helps others to publish to F-Droid. You can do this, too!\nThe folks there have been very kind and helpful in every Merge Requests I looked at (including my own). If someone missed something, that was not a problem at all.\nDespite the few \u0026lsquo;problems\u0026rsquo; I encountered, I feel like the process was straight forward overall. And I learned a ton!\nThanks everyone at F-Droid 💚.\n","date":"27 October 2025","externalUrl":null,"permalink":"/post/2025-10-27-publish-to-f-droid/","section":"Posts","summary":"I published SwatchIt, a native Android app I have been working on, to the F-Droid Android Store 🎉. Now you can get it on F-Droid.\nAll apps published through F-Droid are free and open source software (FOSS) and F-Droid itself is free and open source as well.\nIf you don’t already have F-Droid installed, you should go ahead and install it, it is not as complicated as it looks even on certified Android devices (for now.. but that is a topic for another post).\nPublishing to F-Droid comes with some efforts, mainly since every app that you download through F-Droid must be built by F-Droid as well, so it is not enough to upload an APK.\n","title":"Publish an Android App to F-Droid","type":"post"},{"content":"I started writing a native Android application with Jetpack Compose at the beginning of this year. I have been working on it in my free time and it solves a problem I have been having since I started knitting: Swatch Management.\nWhat is a Swatch? # A swatch is a small piece of fabric that you knit with the yarn and needles in the pattern you intend to knit your finished piece with. You treat your knitted swatch just as you will treat the finished piece (wash it, dry it) and then you take measurements that will give you the following information: How many stitches and rows do I knit per my reference length (usually 10 cm is good enough) and the result is then called your gauge. Gauges vary a lot, from knitter to knitter but also depending on needle size and knitting pattern and style (i.e. knitting in the round versus knitting flat).\nWhy do you need an app for that # Knitting swatches is dull! I want to start with the fun part. So I have often bought a yarn I have knitted with before, because that meant that I knew the gauge. So for every swatch I had to knit I stored the information that was important in a Nextcloud Notes file and on Ravelry. On Ravelry I had to misuse the notes section to add all the information I needed. For example I usually knit 2 swatches for sweaters that have colorwork in it, because my tension is way different when I knit with 2 strands vs when I knit with 1, so I need to account for that in stitch count when I knit the final sweater. I also want to save the measurements I take before I calculate the actual gauge, I often measure at 3 different places on the swatch and average over those measurements. I wanted to develop an app just because I think that that\u0026rsquo;s fun.\nWhy am I writing this? # Marvin and my colleagues reminded me that I should not wait to publish my code until I am satisfied with everything. I need to feel at least a little ashamed. So I published the code on Codeberg! The pipeline still runs in the private GitLab instance I have been using, I will see if I can move that to Codeberg as well. I plan to publish the app in F-Droid as a next step. And I think I will write a few lines on the technical stuff in another blogpost.\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e katharinad/SwatchIt Kotlin 0 0 ","date":"19 September 2025","externalUrl":null,"permalink":"/post/2025-09-10-swatchit-on-codeberg/","section":"Posts","summary":"I started writing a native Android application with Jetpack Compose at the beginning of this year. I have been working on it in my free time and it solves a problem I have been having since I started knitting: Swatch Management.\n","title":"SwatchIt on Codeberg","type":"post"},{"content":"I am Katharina, originally from Germany, now living with Marvin and our children just outside of Borås in Sweden.\nI graduated in 2014 from the University of Duisburg-Essen with a Master of Science in Structural Engineering and have been working as a Software Engineer since then. I started out developing structural engineering analysis applications (think: how big does this cantilever wall have to be given those environmental parameters). I also helped develop an application showing tunnel machine conductors the right way. I have since then moved on to the web and the cloud. I currently work for factor10 as a Principal Software Architect and consultant.\nI have various other interests as well. I sew and knit almost all of my clothes, I grow vegetables and fruits, I play the guitar, I bake and I keep backyard chickens.\nThis blog will be mostly about Software Engineering. Feel free to reach out!\n","externalUrl":null,"permalink":"/page/about/","section":"Pages","summary":"I am Katharina, originally from Germany, now living with Marvin and our children just outside of Borås in Sweden.\nI graduated in 2014 from the University of Duisburg-Essen with a Master of Science in Structural Engineering and have been working as a Software Engineer since then. I started out developing structural engineering analysis applications (think: how big does this cantilever wall have to be given those environmental parameters). I also helped develop an application showing tunnel machine conductors the right way. I have since then moved on to the web and the cloud. I currently work for factor10 as a Principal Software Architect and consultant.\n","title":"About me","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/page/","section":"Pages","summary":"","title":"Pages","type":"page"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"I am available for public speaking and workshops. Feel free to contact me via email or Sessionize.\nUpcoming: # DDD Europe 2026 10.-12.06.2026 Just start (with Value Objects) My talk about Value Objects will be part fo the Foundations Track! Past: # Scan-Agile 2026 18.03.2026 Just start (with Value Objects) Link to slides\nFoo Café 22.01.2026 Just start (with Value Objects) ","externalUrl":null,"permalink":"/page/speaking/","section":"Pages","summary":"I am available for public speaking and workshops. Feel free to contact me via email or Sessionize.\nUpcoming: # DDD Europe 2026 10.-12.06.2026 Just start (with Value Objects) My talk about Value Objects will be part fo the Foundations Track! Past: # Scan-Agile 2026 18.03.2026 Just start (with Value Objects) Link to slides\n","title":"Speaking","type":"page"},{"content":"I created SwatchIt, an Android app that helps you manage your knitted swatches. The source code is openly available on Codeberg. \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e katharinad/SwatchIt Kotlin 0 0 What is a swatch? # Every knitter knits with a slightly different tension. Meaning that two knitters that use the same yarn, needles, pattern and knitting style (Contentional, English, etc.) won\u0026rsquo;t necessarily produce the same fabric. Some people knit tight, others knit loose and changing your knitting tension is not something you can change with sheer willpower. It is more like riding a bike, once you can do it, you are no longer aware of how you do it.\nIf you want your knitted object to be a specific size, you need to know your tension. Therefore you knit a small piece (usually 10 x 10 cm) of test fabric. This is called a swatch and the process is referred to as \u0026lsquo;swatching\u0026rsquo;.\nThe swatch should replicate your real project as much as possible. You use the same knitting needles, yarn, pattern and treatment (e.g washing and drying) as you intend to use later with the real project.\nYou then measure how many stitches (horizontal) and how many rows (vertical) you needed to knit to get to a reference width and height (usually 10 cm).\nYou usually take at least 3 measurements at different places and average over these measurements.\nThe result from this is called your gauge. You can then use cross-multiplication to calculate any widths, heights, stitches and rows you might need.\nWhat is the problem? # Nobody I have ever met likes knitting swatches (including myself, of course!). A lot of people never knit them. But as soon as you alter patterns or make your own designs you won\u0026rsquo;t get away with estimating your gauge. I know because I really tried for a long time!\nMaking a swatch is a long-ish process (and so boring!) and it has happened to me more than once that I had forgotten which needle size I used once I had calculated the gauge. I also always need to write the measurements down at the exact moment I take them, I will forget them otherwise. For every project I need to write down a lot of things and the potential of missing or forgetting something is high.\nI also realized that instead of swatching for every project I could just always buy the same three yarns. I have swatched my favorite yarns once and reuse the gauge for every new project using these yarns. So for me it is important to keep a list of all my past swatches with all relevant information to be able to reuse them for the next project.\nI have been using Ravelry for keeping track of my swatches in the past. But Ravelry is project based, you can only store gauges (not measurements) and you need to remember in which project you might have used the same yarn and needles and pattern to find the gauge you are interested in. There is also only room for one gauge per project, but I often need to knit more than one swatch per project.\nAlso Ravelry is closed source and my data is stored somewhere, probably in the US. I want this data to belong to me. And I don\u0026rsquo;t see a benefit in sharing it with others as it is so individual.\nWhat is the intended use? # SwatchIt supports my flow. First you can create a swatch, probably before you have started knitting it. You can add information about the yarn, the pattern, the knitting needles and the swatch.\nThe swatch you just added is then shown in the main view with some of the information you entered:\nYou can always add or change information about the swatch by tapping on it. This will bring you to an edit screen. You can even add a picture of your swatch: When you are ready to record measurements you can do so by clicking on the \u0026lsquo;View measurements\u0026rsquo; button in the edit screen. When you have added at least one stitch and one row measurement you can let SwatchIt calculate the gauge for you (click on the Calculate button). The gauge is calculated separately for horizontal and vertical measurements, averaged over all measurements to a reference of 10 cm and rounded up to the next full number. I decided to always round up because it is better to end up with a slightly too loose than a slightly too tight garment.\nOnce your swatch has a gauge, you can use it for calculations. Click on the calculations button and let SwatchIt calculate stitches from a width (and vice versa) or rows from a height (and vice versa).\nAll data is stored locally on your phone only. If you switch phones you can create a backup of your data and restore the backup on another phone. Click on the menu on the top right in the main view and choose \u0026lsquo;Backup\u0026rsquo;.\nWhere can I download SwatchIt? # You can get SwatchIt from the F-Droid Android store. F-Droid only distributes open source Android apps, all apps are build from source. This makes it easier to detect and ban malicious apps or apps that track their users or have other unwanted properties.\nTheir documentation explains how to install F-Droid. And I have written a blogpost on what steps I took to publish the app to F-Droid.\nYou can also download the apk directly from Codeberg and install it. You will have to keep track of new versions by yourself though.\nI don\u0026rsquo;t support proprietary app stores like the Play Store or the Apple Store. I won\u0026rsquo;t go through Google\u0026rsquo;s Developer Verification. In case Google will make their threat a reality then SwatchIt will no longer run on Google Certified Devices (e.g. no custom roms like Lineage or Graphene OS). Google should not be allowed to control what you install on your phone. Please help keep Android open.\nHow can I submit feedback? # I very much welcome feedback. Reach out via a channel of your choice or create an issue on Codeberg.\n","externalUrl":null,"permalink":"/page/swatchit/","section":"Pages","summary":"I created SwatchIt, an Android app that helps you manage your knitted swatches. The source code is openly available on Codeberg. \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e katharinad/SwatchIt ","title":"SwatchIt","type":"page"}]