Software development: defining the architecture or working within an existing one.
I have defined the architecture of projects for which I held technical responsibility. Most of the case studies published here, however, describe consulting assignments where I joined established teams and products. My scope for making decisions differs greatly between these situations, and so does the way I apply methods and patterns.
When I design the systemI choose its structure and technologies based on the problem, constraints and people who will maintain it.
When I join a product teamI understand the existing decisions, follow team conventions and contribute within my assigned scope.
Two ways of working
01
When I define the architecture
In assignments where I am responsible for the design, I define the architectural choices and take responsibility for them.
I start with requirements and constraints, then set module boundaries, choose the stack and decide how the parts communicate. From the outset, I consider who will build, test and maintain the system. I introduce a pattern when it solves a real problem or makes the application easier to evolve.
Decisions I evaluate
Module responsibilities and boundaries
Domain rules and use cases
Persistence and external integrations
Testability, releases and maintenance
02
When I join an existing product
On consulting assignments, the architecture and guidelines are often already in place.
My first job is to understand the system: conventions, dependencies, flows between services and the reasons behind earlier decisions. I analyse, develop and test the tasks assigned to me in coordination with the team. If I find a structural issue, I propose a change that fits the project and the responsibilities of those who own its architecture.
This is the situation reflected in most of the case studies on this site. The how I work page explains the scope of my different roles in more detail.
My contribution
Analysing each task within the wider system
Developing to the team's standards
Testing and preventing regressions
Proposing technical improvements when there is room to make them
Backend and frontend
I have worked in software development for more than twenty-five years. The Microsoft ecosystem is my main reference point, alongside the web technologies each project needs.
On the backend I work mainly with .NET and ASP.NET Core: APIs, application rules, data and integrations. I have followed the platform from .NET Framework to current .NET versions, working for small and medium-sized businesses as well as in enterprise settings.
On the frontend I have extensive experience with Angular: first AngularJS, then Angular from its earliest versions. I have also used React on large projects. The product and the team determine the choice of tool; in either case, I aim to build interfaces that remain manageable as features and teams grow.
Organising code when I can make the choice
I do not start with the name of an architecture. I start with the rules the code must express and the changes it will need to support.
In modules with significant business logic, I separate the domain, use cases and technical details. The domain contains the customer's concepts and business rules; the application layer coordinates operations; databases, messaging and external APIs are integration details. This applies the central principle of Clean Architecture: system rules should not depend directly on the technology used to store or expose them.
I often organise work by feature with a Vertical Slice approach. The endpoint, request, handler and validation for a use case stay close together, so someone changing a feature can follow its path without searching distant folders. Clean Architecture and Vertical Slice address different questions: the former concerns dependencies, the latter how the code is grouped.
I use dependency injection to make dependencies explicit, and separate commands, queries and their handlers when that clarifies the use cases. These tools need restraint: extra layers and abstractions can make a simple feature harder to follow.
Clean Architecture, simplified: code dependencies point towards application and domain rules; infrastructure implements interfaces required by the application.
Vertical Slice: files for one feature stay together instead of being spread across folders by technical type.
Microservices and event-driven communication
Separating modules within an application and deploying independent services are different decisions.
I consider microservices when functional boundaries are clear and there is a real benefit to evolving or releasing the parts independently. That autonomy has a cost: network communication, error handling, monitoring and release coordination. In other contexts, one application with well-defined modules may be more suitable.
I have worked on projects using Azure Service Bus or RabbitMQ for asynchronous communication. One component can publish an event and others can react without the producer calling each recipient directly. This decoupling requires care with delays, duplicate messages, errors and traceability. In existing products I work within the established architecture, helping with integrations and diagnosing flows; when I own the design, I evaluate whether this approach is justified by the problem.
FrontendUser interface
→
API GatewayAPI entry point
Customer ServiceCustomersCustomer DB
Order ServiceOrdersOrder DB
Order ServicePublishes OrderCreated
→
Message busAsynchronous delivery
→
Billing ServiceReacts to the event
Illustrative example: services have separate responsibilities and data. Some flows use APIs, while others use asynchronous messages. This split needs clear functional boundaries and adequate operations.
Order ServicePublishes OrderCreated
→
Message brokerAzure Service Bus or RabbitMQ
BillingPrepares billing
WarehouseUpdates inventory
NotificationSends a message
Event-driven communication, illustrative example: the producer publishes once and interested consumers respond independently. Each consumer must also handle delays, duplicates and failures.
Domain and testing
Important rules should be understandable to people who know the product and remain testable over time.
I apply Domain-Driven Design ideas to the extent the problem calls for: a shared language with product experts, boundaries between functional areas and business rules placed where they can be understood and tested. Not every application needs every DDD pattern; in team projects, I start with the patterns already adopted and the decisions of those leading the architecture.
Automated tests check behaviour and protect future changes. I use TDD when writing a test first helps pin down a rule or an edge case; in other situations I add tests during development or reproduce a bug with a test before fixing it. Testing an API is also useful before its client is ready. Where tests are required for a pull request to be approved, I follow the team's requirements. A coverage percentage, however, is different from the quality of the checks.
Complexity proportionate to the problem
Architecture should help a project grow, rather than become an end in itself.
A system that exchanges events does not necessarily need event sourcing. With event sourcing, events form a persistent history from which the system's state is rebuilt. This may help when a process's evolution must be preserved, but it adds work around versioning, projections and long-term management. I would consider it only for a concrete need.
I apply the same criterion to layers, patterns and distributed services: start with a proportionate structure and evolve it when rules, boundaries or loads justify a more elaborate solution. In projects I lead, I take responsibility for those choices; when I work on someone else's product, I help improve it within the team's responsibilities and constraints.
Need to design an application or evolve an existing product?