Six Years Of Full-Stack Development And What It Taught Me
Six years is long enough for software development to stop feeling like a collection of tools and start feeling like a way of thinking. I began with the familiar excitement of making a page respond to a click, then gradually found myself responsible for the less glamorous parts: data migrations, deployment scripts, authentication flows, test suites, monitoring, and the small decisions that determine whether an application remains pleasant to change.
My work has moved across the full stack, from Angular components and browser behaviour to .NET services, C# code, SQL Server queries, and the infrastructure connecting them. That breadth has been useful, but it has also exposed a tension at the centre of modern development. We are encouraged to learn everything, while real projects reward people who understand a few systems deeply enough to make sound trade-offs.
Writing for an Australian audience adds an interesting perspective. A developer in Melbourne may be collaborating with a team in Perth across several time zones, while someone in Brisbane is dealing with a market shaped by government contracts, start-ups, agencies, and large enterprises. The technology is global, but the working conditions, hiring expectations, and everyday language are distinctly local.
The Full Stack Is A Series Of Boundaries
The phrase “full-stack developer” can sound as though one person is expected to master an entire software company. In practice, it means becoming comfortable at the boundaries between systems. A browser sends a request, an API interprets it, a database stores something, and a user eventually sees a result. The difficult work often happens in the hand-offs.
Early in my career, I tended to think about features in vertical slices only after the coding had started. I would build a component, then discover that the API contract needed a different shape, then adjust a database query, then work around an assumption hidden in a test. With experience, I began tracing the journey first. What does the user need? What data represents that need? Where should validation happen? Which failure should be visible, logged, retried, or ignored?
This habit matters in Australia’s distributed technology market, where a team might include a product owner in Sydney, developers in Adelaide, and a client in regional New South Wales. A “quick change” can cross several boundaries before it reaches production. Clear contracts and careful communication save more time than clever code.
Tools Change Faster Than Engineering Principles
Angular, .NET, C#, and SQL Server have all evolved during my six years in development. Framework conventions shift, package managers change, and a technique that once looked modern can become maintenance debt. Keeping up is part of the job, but chasing every new release is not the same as growing as an engineer.
The principles that have stayed useful are much less fashionable: name things clearly, keep responsibilities understandable, make failure observable, and write code that another person can safely modify. These ideas apply whether the application uses server-rendered pages, a single-page client, or a collection of services. They also apply when the team is having a rushed arvo before a release and nobody has the patience to decode an overly clever abstraction.
One lesson I have taken from maintaining software is that the best tool is often the one the team can operate consistently. A technically impressive framework is a poor choice if deployment is mysterious or local development takes half a day. A straightforward solution with good documentation can serve a business better than a fashionable architecture assembled without a clear need.
This is especially relevant to Australian organisations that must balance innovation against limited hiring pools and operational budgets. A company in Canberra working with public-sector requirements may value auditability and predictable support more than novelty. A small Melbourne product team may accept more experimentation, but it still needs to know what will happen when the original author moves on.
Testing Became A Way To Think
I once viewed testing primarily as a safety net added after implementation. Over time, it became a design tool. When a class is awkward to test, that awkwardness often points to too many responsibilities, hidden dependencies, or a boundary that has not been properly defined.
Unit tests are valuable for focused behaviour, especially when business rules have many combinations. Integration tests reveal whether an API, database, configuration, and authentication system cooperate as expected. Browser tests provide confidence in important user journeys, though they also require discipline because slow or fragile tests can become a burden nobody trusts.
The challenge is deciding what deserves confidence and how much. A test that asserts every internal detail may break during harmless refactoring. A test that checks only a successful response may miss the conditions that cause real users trouble. The most useful suites describe behaviour at the right level, cover meaningful failure paths, and make it safe to improve the implementation.
I have also learned that testing is a communication practice. A well-written test tells a future developer what the system promises. That is valuable when a team grows, when a consultant hands a project over, or when a developer returns to code after several months. In a market where people frequently move between contract roles and permanent positions, executable examples can preserve context better than a long wiki page.
Automation Made The Browser More Reachable
Some of the most enjoyable projects have involved taking browser behaviour out of the browser. Headless Chrome makes it possible to load pages, interact with controls, capture content, and verify flows without manually operating a desktop. That capability connects web development with data collection, regression testing, reporting, and practical automation.
Working with Puppeteer Sharp showed me how approachable browser automation can be from a C# application. The interesting part is not simply opening a page and reading its HTML. Reliable automation requires waiting for the right state, dealing with navigation, handling timeouts, respecting access rules, and deciding what should happen when a page changes. A small script can become a fragile system if those details are ignored. My experience is captured in this guide to Puppeteer Sharp crawling, which remains a useful example of connecting familiar .NET code to a real browser.
This work also changed how I think about web interfaces. A page is not merely a document to display; it is a sequence of states. Buttons may appear only after a request completes. Content may be rendered client-side. A cookie banner may block an interaction. Designing with those states in mind improves both automation and accessibility.
Australian teams often need this kind of practical automation for internal systems, retail sites, property workflows, and reporting tools. A business operating across Queensland or Western Australia may have repetitive browser-based tasks that do not justify a large platform rebuild. A small, carefully maintained tool can remove hours of routine work, provided it has clear limits and monitoring.
Career Growth Is Less Linear Than It Looks
From the outside, six years can look like a neat progression from junior to experienced developer. The reality is messier. There are stretches where learning feels rapid, followed by periods dominated by maintenance, meetings, or a project whose technical constraints leave little room for experimentation. Progress is often visible only in hindsight.
I have become less interested in proving that I know a particular technology and more interested in understanding why a decision was made. Was a database choice driven by reporting needs? Did a service boundary emerge from team ownership? Was a legacy pattern a temporary compromise that simply survived? These questions lead to better conversations than treating every older system as evidence that previous developers were careless.
Immigration has added another layer to that reflection. Moving countries makes familiar professional assumptions visible. The vocabulary may change slightly, workplace expectations may differ, and a career history that made perfect sense in one place may require explanation in another. Building a life in Stratford, Ontario, while working in an international industry has reinforced the value of patience, clarity, and the willingness to start conversations from first principles.
That perspective is useful for developers considering the Australian market. The language may include “uni,” “FIFO,” “mates,” or “no worries,” but the underlying professional questions remain consistent: Can you explain your decisions? Can you collaborate when requirements are unclear? Can you learn a codebase without dismissing its history? Strong communication travels well, whether the office is near Sydney’s train network or a remote team spread across the country.
Software Work Also Reflects Character
Technical choices can reveal how people respond to uncertainty. A team that hides errors may be uncomfortable admitting that systems fail. A team that writes clear operational documentation may understand that responsibility continues after deployment. A developer who asks for context before making a large change is not being slow; they may be protecting the project from an expensive assumption.
This is one reason I enjoy writing about subjects outside programming. Thinking about moral agency, responsibility, and the choices people make can sharpen the way we approach technical work. A thoughtful exploration of Tolkien’s view of evil offers a different lens on corruption, absence, and deliberate action. The connection to software is indirect, but useful: systems rarely become unhealthy through one dramatic event alone. Small omissions, unexamined incentives, and tolerated shortcuts can gradually shape their character.
That does not mean turning every code review into a philosophical seminar. It means remembering that engineering affects people. A rushed authentication change can expose private information. A poorly designed form can exclude users. An automated decision can create a result that is technically consistent yet plainly unfair. Technical competence needs judgement alongside it.
After six years, I am more conscious of the human consequences of software than I was at the beginning. The goal is not to eliminate uncertainty or make perfect systems. It is to make choices deliberately, communicate risks honestly, and leave the code and the people around it in a better state.
Practices That Still Pay Off
The habits below have remained useful across languages, frameworks, clients, and stages of a project:
- Trace a feature from the user interface through the API, data store, deployment process, and operational feedback.
- Prefer clear boundaries and modest abstractions over designs that require a long explanation before anyone can contribute.
- Test important behaviour, including failure paths, rather than measuring quality by coverage percentage alone.
- Treat logs, metrics, documentation, and deployment scripts as part of the product.
- Keep learning broadly, while developing enough depth in a few areas to make reliable decisions.
- Make code reviews about shared understanding and risk, not personal taste or displays of cleverness.
These practices are deliberately ordinary. Their value comes from repetition. A developer who applies them consistently can contribute to a small Australian agency, a global remote team, or a heavily regulated enterprise without needing the same architecture in every setting.
The next stage of my development is likely to involve fewer dramatic technology changes than I once expected. I still enjoy learning a framework or exploring a new open-source project, but I am increasingly drawn to sustainable systems: software that can be understood by the next person, operated by a busy team, and adapted when the business changes direction.
Six years in, full-stack development feels less like standing at the centre of every layer and more like helping those layers cooperate. The work is still technical, but it is also analytical, social, and occasionally philosophical. Build something small, inspect the boundaries, test the important behaviour, and keep asking what the software makes possible for the people who depend on it.