Nikita Lozgachev is a software engineer and functional programming advocate, specializing in reducing complexity, crafting clean abstractions, and driving framework-agnostic architecture.

Email LinkedIn GitHub RSS

Meditations on Code

On Building Systems That Endure

This is for those who care not only about releasing code, but about shaping it with intention and care. There is a difference between simply delivering software and building something that lasts — something you won’t dread returning to a year from now.

Always build as if you will be the one maintaining what you create. Write code not in haste, but with foresight and respect for those who will come after — especially when that person is you. Every shortcut you take today becomes a cost someone must pay tomorrow.

The ease or pain a system causes in the future is a direct result of the discipline applied during its creation. Choose clarity over cleverness — not because cleverness is wrong, but because clarity survives. Clever solutions are tempting, but they often conceal more than they reveal.

Favor the solutions that are reliable and predictable, even if they seem dull at first. The boring approach that works consistently is far more valuable than the brilliant trick that fails in unexpected ways. In the end, good code is not measured by how impressed you are when you write it, but by how relieved you are when you read it again.


On Purity

Make it a habit to ask yourself this question with every function you write: Can this function be pure?

Purity in functions brings a kind of peace — a certainty that data is unchanged by hidden forces or shifting states. A pure function is a clear agreement between your intention and its behavior, free from surprises. These functions become your most reliable tools: easy to compose, simple to test, and resilient across time.

Avoid mutation as if it were a source of corruption. State carries weight — more than just data, it carries complexity and risk. Carry only what is necessary. Pass state explicitly and return new values rather than hiding changes in closures, globals, or side effects that are hard to track.

Reducing confusion in your code is a form of kindness — kindness to yourself and to those who follow. Give every function a name that precisely describes what it does. Let every function signature serve as a promise, and honor that promise without exception. Clarity and reliability are the foundation of trust in your code.


On State

State is weight — a burden that slows every step you take if carried unnecessarily. When you share it carelessly across your system, you invite fragility and confusion. Keep state close to where it originates. Guard it fiercely against leaking into distant or unrelated parts of your code.

Favor immutability whenever possible. Immutable data brings clarity and confidence. When your data does not change unexpectedly, your reasoning becomes simpler, your bugs easier to find, and your system more predictable. Just as the mind seeks order, so does the machine.

Make the flow of data explicit. Changes should never be hidden or mysterious. When data mutates, let it be intentional and clear. Every transformation should be transparent and, where possible, reversible. This openness is the foundation for understanding, debugging, and evolving your system with confidence.


On Naming

A good name is like a lantern in the fog — it casts light on your intention, clarifies purpose, and guides anyone who reads your code through uncertainty. Conversely, a bad name is like a stone in your shoe — small, yet persistent and deeply irritating.

Names should reflect what something does or represents, not just what it is. Like function signatures carry types, names must carry clear and meaningful information that helps others understand your code without guesswork.

When you hesitate or feel confusion, don’t settle. Rename. Modern tools make renaming effortless, and rewriting code is no different than revising a written draft. Fearlessly refining names is never a waste of time — it is an investment in clarity, readability, and long-term maintainability.


On Feature Growth

Think of code as a carefully tended garden. Add new growth sparingly and prune often. Resist the urge to pile on features for their own sake; instead, seek coherence and harmony. A new feature should fit seamlessly — like a stone placed precisely in a sturdy wall — not as a hastily applied patch over a crack.

Before you add anything, ask yourself: What complexity am I introducing? What future maintenance costs will this incur? How will it affect the mental model others must hold to understand the system?

Value orthogonality — design each piece to stand on its own, fully self-contained. If a feature can be built or modified without touching a dozen unrelated files, it speaks to good design. If it demands understanding the entire system just to make a change, it’s a sign the design needs rethinking.


On Onboarding

Every system you build is, in some way, a conversation — a quiet dialogue between its original creators and those who will read, maintain, and extend it in the future. When you write code, structure a system, or leave behind documentation, you are not just solving today’s problem. You are speaking to someone you may never meet, someone who will try to understand your choices and continue your work.

Speak to them clearly and respectfully. The person who inherits your code is not your competitor, and not your critic — they are your continuation. They will live with your decisions. You have the opportunity to make their work easier, their confusion less, their time better spent.

Don’t write guides, comments, or documentation just to fulfill a process requirement. Do it because it is generous. Share what you’ve learned. Explain why you made certain choices and what alternatives you considered. Document trade-offs honestly and expose the assumptions your code depends on. These notes are not fluff — they are lifelines.

Favor structure over chaos. A sprawling, disorganized codebase creates invisible friction. A well-structured one offers orientation, direction, and confidence. When someone new joins the project, they shouldn’t have to guess how it fits together. They shouldn’t feel like they’re being tested. They should feel welcomed — like the system is ready to meet them halfway.


On Simplicity

Simplicity is often misunderstood as a lack of ambition or capability. But true simplicity is strength refined — power expressed clearly, without unnecessary complication. It is the discipline to say no repeatedly, thoughtfully, and with conviction. Complexity, by contrast, often masks hesitation — a reluctance to face difficult trade-offs head-on.

A simple piece of code fulfills its purpose fully, and nothing beyond that. It does not rely on luck or hidden conditions. It surprises no one, because its behavior is clear and predictable. Such code can be understood at a glance, tested confidently, and rebuilt without second guessing.

Aim to write code that explains itself. When something feels obscure or confusing, don’t add more layers; strip away the excess until what remains is obvious. Whether it’s a single function, a file, or an entire module, each part should be able to declare with quiet confidence: “This is what I do.”


On Tooling and Automation

Your time is one of your most valuable resources, yet it is all too easy to waste it on repetitive, mechanical tasks that add no real value. The true power of engineering lies in thoughtful problem solving, not in performing the same steps over and over. This is where automation becomes essential — a way to let machines do what they excel at so you can focus your energy where it truly matters.

Think of automation not as an optional convenience, but as a fundamental tool of craftsmanship. A code formatter is not merely a style preference; it is a source of freedom, ensuring consistent code that requires less mental overhead. Linters are not burdensome nags but reliable guides that keep your codebase healthy and your mistakes visible before they grow. Continuous integration is not needless bureaucracy; it is your safety net, catching problems early and protecting your work from unintended consequences.

Approach automation as you would a trusted colleague — one who is tireless, dependable, and precise. The more you cultivate that trust, the more you free your own mind to tackle complex, meaningful challenges. Automation lifts the weight of the mundane, allowing creativity and insight to flourish.


On Tests

Tests are often misunderstood as constraints — cages that limit freedom and slow progress. In truth, they are the opposite: they are contracts, clear promises that certain behaviors must remain constant no matter what changes come. They declare with certainty, “This must remain true.”

The most effective tests are precise and focused. They cover small, well-defined behaviors, run quickly, and communicate their purpose clearly to anyone who reads them. These qualities make tests reliable tools that support development rather than burdens that slow it down.

When writing tests, focus on the behavior of the system rather than its internal implementation details. Test the boundaries where your code interacts with the outside world, not the private inner workings that may change often. If a test fails frequently, it’s usually a signal that the underlying code needs attention — not that the test itself is flawed.

Write tests early, before mistakes accumulate and regrets set in. They are an act of kindness to your future self, a filter that catches errors before they become problems, and a guide that makes refactoring and improvement possible without fear. Investing in good tests is investing in sustainable, confident progress.


On Refactoring

Refactoring is more than just rewriting code; it is a deliberate act of respect and care. It is your way of telling the code, “I see the journey you’ve been on. I see where you struggled, and I want to help you become clearer, simpler, and stronger.”

Don’t hesitate to change code simply because it works. Working code that is fragile, confusing, or brittle only postpones problems — it does not solve them. True craftsmanship means embracing the discomfort of change to create lasting clarity.

Approach refactoring as a series of small, controlled improvements. Each change should be focused, minimal, and reversible — a single step toward a cleaner path. Over time, these incremental efforts build momentum, transforming the entire codebase into something easier to understand, safer to extend, and a joy to maintain.

Patience and persistence in refactoring are investments in the future — yours and everyone else’s.


On Composition

Composition is the foundational art of building complex systems from simple, reliable pieces — smaller truths that come together seamlessly. When done well, composition allows a system to grow organically, maintaining clarity and coherence rather than becoming tangled and unwieldy.

Focus on composing functions rather than classes; compose behaviors instead of deep hierarchies. Functions, by their nature, are self-contained and predictable, making them ideal building blocks for larger constructs. When you find something difficult to compose, it’s a clear sign that it should be broken down into smaller, more manageable parts.

The best abstractions are those that become invisible — they serve their purpose so naturally that you hardly notice them. Favor combining small, focused pieces over extending large, complex ones. Prefer chaining simple transformations rather than introducing multiple branches of conditional logic.

Avoid inheritance. Though common, inheritance is a brittle fiction. It assumes too much about the relationship between objects, hides important details, and creates fragile dependencies that break easily under change. Instead, favor containment and explicit construction — assemble your systems from data and behaviors rather than deep type hierarchies. This approach leads to code that is more flexible, understandable, and resilient.


On Reusability

True reusability is a gift to your future self — write once, read many. But it’s not about chasing abstraction blindly; it’s about freeing yourself from needless repetition.

Patterns reveal themselves over time. Extract them too soon, and you risk chasing shadows — abstractions that don’t fit and only confuse. Extract them too late, and you drown in duplicated effort and wasted time.

A reusable component or function should be self-contained and context-light. It should accept data, transform it cleanly, and produce clarity. The more it depends on the outside world, the heavier it becomes — and the less useful it is anywhere else. Keep it simple. Keep it portable.


On UI Composition

A user interface is not just pixels — it is logic made visible. Every button, list, or form is an expression of state and structure. The same principles that guide clean functional code should guide how we build interfaces: composition, clarity, and isolation.

Structure your UI like a chain of pure functions. Each component should do one thing, take inputs, and return predictable output. Avoid cramming conditional logic into the render phase — if you’re branching, ask yourself why it wasn’t handled earlier. Push decisions upward. Let the leaf nodes remain dumb.

Name your components after what they do, not how they look. Let their names speak intention, not mechanics. If a piece toggles visibility, make that decision elsewhere. If a piece displays a form, make that obvious in both name and shape.

Build small, legible components you can reason about in isolation. Let reuse emerge naturally through composition, not through force. The test of a good UI is not only how it feels to the user — but how easy it is to change without breaking your mind.


On the Interchangeable and the Irreplaceable

In every company, every team, every system — people come and go. You will too. Someone will eventually take your place, pick up where you left off, and carry the work forward. This isn’t failure. It’s design.

But replacement is not replication. The role may be filled, but the way you filled it — that is yours alone. The care you took in naming things. The patterns you chose to follow or reject. The questions you asked when others stayed silent. These things don’t live in the codebase. They live in the experience of working with you.

You won’t always be remembered. But your thinking will echo in the clarity you leave behind. Don’t aim for irreplaceability — aim for unmistakable quality. Quiet, careful work that reflects who you were, even long after you’re gone.


On Who You Work With

The people you work alongside will shape more than just your output — they will shape your habits, your expectations, and your sense of what’s normal. It’s easy to underestimate how quickly mediocrity spreads. You find yourself explaining what should be self-evident, defending good practices against indifference, and lowering your standards just to keep the peace. At first, it feels like compromise. Over time, it becomes decay.

The cost of working with people who don’t care — or worse, don’t understand but pretend they do — is more than inefficiency. It’s erosion. Of your clarity, your motivation, your professional pride.

Instead, choose to work with people who challenge you — those who ask sharp questions, who improve your thinking just by being in the room. People whose presence makes you better, not smaller. That kind of environment isn’t just pleasant — it’s protective.

Your standards are not fixed. They are shaped daily by the people around you. Choose wisely.


On the Danger of No Resistance

It’s easy to enjoy being the one with all the answers — the person others look to when they’re stuck, the one who can untangle the toughest problems with ease. But comfort is a subtle trap. When every challenge feels familiar, when no one around you pushes back or sees what you’ve missed, you stop sharpening your edge without realizing it.

Growth demands tension — the kind that comes from being around people who are better than you at something, who see the world differently, who ask questions you wouldn’t think to ask. That friction doesn’t just test you; it reshapes you.

If every discussion feels one-sided, if you’re never surprised or humbled, you may have stayed too long. The goal isn’t to feel superior — it’s to stay in motion. Find the rooms where your ideas are challenged, your assumptions corrected, your skills expanded. The right kind of discomfort is not a threat; it’s a sign you’re exactly where you need to be.


On the Nature of the Business

There is a quiet but crucial difference between writing software and being allowed to shape it. In some companies, IT is viewed as overhead — a necessary but inconvenient cost. In those places, engineering is seen as a service desk, not a strategic asset. You’re not expected to think, just to deliver faster, with fewer questions, and often with fewer people.

Over time, that environment wears you down. Every decision becomes reactive. Every improvement feels optional. Your work is measured in throughput, not in insight.

Contrast that with companies where software is the business — where the code you write directly impacts what the company offers, how it grows, and whether it survives. In those places, engineering is not a cost center; it is a core function. Product and engineering move as one team, not as silos negotiating priorities.

Work in places where your thinking matters — not just your output. When software drives the business, your choices are taken seriously, your time is valued, and your effort builds more than just features. It builds momentum.


On Not Meeting Your Heroes

There is a comfort in admiring someone from a distance. You imagine their work is effortless, their insight innate, their success inevitable. You fill in the blanks between what they do and what you understand with reverence — believing they see the world in some higher resolution, that they operate in a way that people like you simply do not.

But meeting them — truly seeing how they work — dissolves the illusion. You realize they’re navigating with the same doubts and guesses. They miss things. They revise constantly. They backpedal and improvise. And the more time you spend around them, the clearer it becomes: they are not following some hidden script. They are figuring it out live.

This realization can be disorienting at first, even disappointing. But sit with it longer, and it becomes something else — grounding. Liberating. The distance between you and them shrinks. You don’t need to become someone else to do meaningful work. You just need to keep showing up, thinking critically, and refining your judgment.

Let your respect evolve. Don’t aim to become a hero. Aim to become someone real — someone worth learning from, precisely because you don’t pretend to know it all.


To Yourself

Every decision you make in code — every shortcut, every clever trick, every missed simplification — becomes someone else’s burden. Often, that someone is you. Complexity may feel like progress in the moment, but it is almost always a form of debt. And like all debt, it comes due.

There will be pressure to go faster. To compromise. To leave things “good enough.” Resist that where you can. The time you think you’re saving today often becomes the time someone else has to pay for later — in confusion, in rework, in frustration.

Write code you’re proud to return to. Build systems that explain themselves. If you stumble, which you will, correct it with care. And when you move on, leave behind something better than what you found — even if just a comment, a refactor, or a thought like this one.

Your work will outlive your presence. Make sure it deserves to.