ADVICE to my younger self as a Software Developer

What advice would I give a software developer? Software development, design, and architecture can feel overwhelming. Especially when you’re just trying to get into it as there is so much to learn. What exactly should you focus on? A particular framework, language, or library? After over 20 years of software development as a career writing line of business and enterprise systems, here are the top 5 pieces of advice I’d tell my younger self.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Business Knowledge

If you’re writing Line of Business or Enterprise Software, having a good understanding of the business domain your writing software for is critical. This is why it’s perplexing to me that the software industry is so focused on frameworks, libraries, and platforms. Yes, technical concerns matter, but the sheer lack of emphasis on business concepts is amazing to me.

By far the best developers I know have a good understanding of business concepts and the specific business domain they are in. This gives them the ability to speak to a varied audience. From both spectrums, they can speak with domain experts all the way to other software developers.

Having experience in many different business domains only helps your understanding of designing a system. There is a common thread that generally is involved in most business domains that you can then relate to others.

My advice to my younger self is don’t get so caught up entirely on the technical aspects of being a software developer. I wish I would of put more focus on understanding various business concepts earlier in my carrier.

Logical Boundaries

The more you understand how various businesses domains operate, the better you can understand how to design a system by decomposing it into logical boundaries.

Defining boundaries is one of the most difficult things to do, yet is one of the most important.

ADVICE to my younger self as a Software Developer

When you don’t really know a particular domain, it’s like walking into a dark room with only a small flashlight. As you point the flashlight around the room, you get a better sense of how big the room is, what’s on the floor, how tall the ceilings are, etc. It takes time and it takes a lot of exploration to get a better perspective.

Logical boundaries own a set of business capabilities and the data behind those capabilities. Within a logical boundary, there are long-running business processes and workflows. These also might span multiple logical boundaries.

CRUD (Create-Read-Update-Delete) has its place, but not the heart of the system which generally has a lot of complexity. The outer edge of your system has boundaries that are more in a supporting role for reference data can often be CRUD or be an external system/service that you integrate with.

Define logical boundaries that isolate the business capabilities your system provides.

Coupling & Cohesion

Fundamentally understand Coupling & Cohesion. Seemingly every design decision revolves around these two concepts.

coupling is the degree of interdependence between software modules; a measure of how closely connected two routines or modules are

https://en.wikipedia.org/wiki/Coupling_(computer_programming)

cohesion refers to the degree to which the elements inside a module belong together.

https://en.wikipedia.org/wiki/Cohesion_(computer_science)

Circling back to the earlier section about defining Logical Boundaries is exactly about trying to achieve a high level of functional cohesion. Logical boundaries will have some degree of coupling because they will need to interact with each other.

Ian said it perfectly. It’s all about trade-offs.

Complexity

If you’re in a complex business domain, managing complexity can be difficult. But don’t make it even worst by adding unneeded technical complexity. This happens way too often by developers wanting to use new technology/library/framework without any idea if it solves a problem they actually have.

Complexity also comes in the form of logical boundaries as well as coupling & cohesion.

Defining logical boundaries is trying to prevent you from creating a giant turd pile (big ball of mud). Ultimately you’re trying to build a set of smaller turd piles. This allows you to isolate the complexity to a smaller logical boundary that is limited in scope.

As much as you think the system you’re building is going to be perfect, over time different insights and gaining new knowledge will mean that the overall design will be less ideal than you want it to be. And that’s OK!

Isolate complexity the best you can give your understanding at the time of making a decision. Understand that you won’t get it completely right. However, DO NOT add unneeded technical complexity because a discovered a new design pattern, library, framework, etc that you think will solve all of your problems. Also, DO NOT add unneeded complexity based on assumptions or “what ifs” that you think might be needed to make the system more flexible possibly in the future. Give yourself options but not at a high cost of complexity.

Know what you don’t know

There is so much to learn in software it’s pretty much impossible to know everything. However, it is helpful to have a surface-level knowledge or just be aware of the current landscape. That doesn’t mean you need to be an expert at all, rather the opposite. Just being aware of a new tool/framework/platform and the problems it solves can be really helpful if you find yourself with that problem.

The key is acknowledging that you aren’t an expert but rather only have entry-level knowledge.

Don’t be an Expert Beginner.

My software developer advice is to realize that there is a mountain of knowledge you just don’t have and that you don’t know what you don’t know. That’s OK! Don’t take yourself too seriously.

Join!

Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design. As well as access to source code for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.

Follow @CodeOpinion on Twitter

Software Architecture & Design

Get all my latest YouTube Vidoes and Blog Posts on Software Architecture & Design

Asynchronous Request-Response Pattern for Non-Blocking Workflows

What’s the asynchronous request-response pattern for? We’re used to synchronous communication. You make a request to another service and get a response. When you move to asynchronous communication, you often think of messages as fire-and-forget. You send a command to a queue, and the consumer handles it asynchronously. Because of the temporal decoupling, you don’t know when the message was processed or what the result was. However, there is a solution! Let me explain how you can use the request-response pattern to reply sender once a message is processed.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Blocking Synchronous

Most developers are familiar with blocking synchronous calls. If you make an HTTP call to a service, you’re making a blocking synchronous call.

When Service A makes a blocking synchronous call to Service B, it must wait to get the response (or acknowledgment) that the request is completed. There are many situations where this is entirely appropriate, such as in client or UI code. UI code reaching out to get data from a service is naturally blocking request-response.

However, service to service communication using blocking synchronous calls has complexity. As an example, if an order is placed within the Sales service, it makes a blocking synchronous call to the Billing service to charge the customer’s credit card or create an invoice.

Once that blocking call succeeds, then needs to make a blocking synchronous call to the warehouse service in order to allocate the product and create the shipping label.

This entire process is blocking and is temporally coupled. Both Billing and Warehouse services need to be available in order for the Sales service to place an order. Also since we lack a distributed transaction, we can be left with an inconsistent expected state if there is a failure. If the request to Billing was successful, but the request to the Warehouse failed, we now need to develop some resiliency to make another call back to the Billing service to refund the order. But what if that request fails? This isn’t resilient at all.

Asynchronous Request-Response

One solution to this problem is to remove the temporal coupling by using asynchronous request-response. This way each service can operate independently without requiring the other service to be available and able to process requests. To do this we can leverage asynchronous messaging and also apply the request-response pattern.

Here’s how asynchronous request-response works. First, the producer will send a message to a queue.

Asynchronous Request-Response Pattern for Non-Blocking Workflows

The consumer will process that message asynchronously.

Asynchronous Request-Response Pattern for Non-Blocking Workflows

Once the consumer has processed the message, it will create a reply message and send it to an entirely different queue.

Asynchronous Request-Response Pattern for Non-Blocking Workflows

The producer will then consume the reply message from the reply queue.

Asynchronous Request-Response Pattern for Non-Blocking Workflows

So now let’s jump back to the example of Sales, Billing, and Warehouse services. Using asynchronous messaging and the request-response pattern, we remove the temporal coupling and have each service work independently.

When an order is placed in the sales service, we’ll have an orchestrator that will send a Bill Order command to queue on our message broker.

Once Billing consumes that message, it will then send an Order Billed reply message that the Orchestrator will consume.

Once the Orchestrator consumes the Order Billed reply message, it will then create and send a Create Shipping Label command that the Warehouse service will consume.

Once the Warehouse has processed the message it will create a Shipping Label Created reply message that the Orchestrator will consume.

Once the Orchestrator has processed the reply, it changes the status of the order that was placed to Ready To Ship.

Here’s what our Orchestrator (NServiceBus Saga) looks like. It’s handling the appropriate reply messages and sending new commands (messages) to the broker as an asynchronous workflow.

Because all of this work is done using non-blocking asynchronous messaging, none of the services must available when an order is placed. They are all working independently processing messages in isolation.

The asynchronous request-response pattern allows you to tell a sender that the message has been processed and what the outcome or result was. You can leverage this to then build workflows to involve many different services all in a non-blocking way.

Join!

Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design. As well as access to source code for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.

Follow @CodeOpinion on Twitter

Software Architecture & Design

Get all my latest YouTube Vidoes and Blog Posts on Software Architecture & Design

Anti-corruption Layer for mapping between Boundaries

What does an anti-corruption layer solve? Most systems rely on data or behaviors from another service or an external 3rd party. The problem is they often don’t share the same semantics or data structures. Left unchecked this leads to convoluting up your own boundary with concepts from another boundary. Let me explain how you can use an anti-corruption layer as a way to translate the concepts from another boundary in isolation.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Anti-corruption Layer

A Service is the owner of a set of business capabilities and the data behind those capabilities. Service boundaries are defined by those business capabilities that are highly cohesive. There is generally a specific language and terminology used within a service boundary that also defines it.

When you integrate with other service boundaries or external 3rd party services, they may not share the exact same semantics. The terminology and data structures may not align with yours. This can lead to leaking other service boundaries concepts into your own.

In some situations, in my experience, generally, boundaries that are in more supporting or generic roles, may share the same semantics or at least are very close. An example of this is accounting. Let’s say you have a service boundary for financials for receivables and payables.

Since you don’t have full-blown double-entry accounting in your service, you integrate with a 3rd party external accounting system. In this case, since both your service and the external system are likely aligned in data structures and language, there really isn’t any required translation.

However, in other situations, they may not line up at all. And this is when you can run into trouble. As an example, let’s say we have a system for a food delivery service. One of the services is for handling all the incoming orders.

Now we may integrate with other external 3rd party systems that have an entirely different way of structuring order data as well as the terminology they use to represent what we have in our Ordering service.

To prevent any external concepts from leaking into our service boundary, we can place an anti-corruption layer at the very edge of our logical service boundary. This is about a logical boundary, although it can be a physical boundary. The anti-corruption layer can be physically deployed within our primary service process, or it could also be its own physical process that then communicates to our primary service process. Regardless, this is about logically, not physically.

Anti-corruption Layer for mapping between Boundaries

An external service will interact with an anti-corruption layer so we can translate from their semantics to ours. This means that if you have many different integrations, you may also have more than just one anti-corruption layer. Each anti-corruption layer will be responsible for handling the transaction for each specific 3rd party.

Anti-corruption Layer for mapping between Boundaries

Example

To illustrate this, let’s say we have to make an HTTP call to the external 3rd party service to pull new orders that we need to import into our system.

In this example, we’re creating a PlaceOrder which is a command that will be sent to a queue for processing. The PlaceOrder command is how we place an order within our service boundary. This is the only way to place an order. This means that when we get data from an external service, we need to do some translation to build a PlaceOrder command.

Event Driven Architecture

If you’re using an event driven architecture, this translation comes in the form of translating an event into a command.

Anti-corruption Layer for mapping between Boundaries

When you consume an event that was published by another service boundary, you want to translate that into something meaningful to perform some type of action within your own service boundary. To do that means translating the event into a command.

In the example above, when an OrderRefunded event is published by the Billing service boundary and is then consumed by the Ordering service boundary, it needs to cancel the order. To do that it does the proper translation to build the appropriate command.

Source Code

Developer-level members of my YouTube channel or Patreon get access to the full source for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.

Anti-corruption Layer

An anti-corruption layer is a great way to isolate the logic of translation from external sources into the semantics defined within your own logical service boundary. It’s all about translation and keeping external concepts out and not leaking in and pushing this translation to the outer edge.

The translation isn’t about creating records to interact with your database, it’s simply translation so that you can perform work within your boundary as you understand it.

As with everything, there are tradeoffs. The downsides are you can be adding latency, especially in my example because I’m enqueuing a command instead of immediately creating an order. While not necessarily a downside, you are adding indirection, which does have a cost. Lastly, your anti-corruption layer can get pretty large if you’re interacting with a large external system or many external systems. If you are, don’t be shocked if over time the anti-corruption layer becomes larger (in code size).

Follow @CodeOpinion on Twitter

Software Architecture & Design

Get all my latest YouTube Vidoes and Blog Posts on Software Architecture & Design