Locked into a Contract? Lucky You!
Wait.. what? Contracts are horrible! I don't even know if I'm going to be using that service in the next few months, let alone years. I need flexibility so that as my requirements change I know I'm not going to be locked into something that no longer works for me.
If you've ever had to sign a contract, you may be able to relate. They can be a pretty big deal. Both parties are explicitly agreeing to terms and conditions that cannot be violated, regardless of what may happen in the future. While contracts may feel like a burden in the real world, they are extremely powerful in software.
So what is a software contract? In practice, it doesn't differ that much from traditional contracts. It's an agreement between systems about what functionality is exposed, the data types of each message, the structure of the resource, etc. This allows us to forego a lot of manual validation that is typical when you're not exactly sure what's going to be sent on the wire or what the environment looks like.
Life without Contracts
Consider two independently deployed web services: PingService and PongService. When PingService sends a message to the PongService, the PongService should return a message, letting the PingService know the request was received.
Immediately we're in a state where there are more questions than answers. We know these services can talk over HTTP.. but that's about it.
- How do we structure the message to send to the PongService? Is it expecting JSON? XML?
- What type of data should we send to the PongService? A Boolean? Integer?
The answer that more than likely comes to mind is to.. look at the API documentation! Which, assuming it was kept up to date, should answer those questions for us.
Ok great, lets assume the documentation is up to date and accurate. We find out that it is indeed JSON and is expecting a string. Apparently the PongService just returns the string that was sent as its response.
So the next steps most likely involve creating some model to represent the result of calling the service, importing Newtonsoft.JSON so we can do some JSON serialization and deserialization, and then ultimately POST to the endpoint (or was it a GET request?). After all of that is implemented, we finally validate whether or not both our request and response was formatted correctly.
Now that seems like a lot of work, manual work, to make a web request. The problem gets worse when you also consider that this process would need to be done for each and every service that wanted to interact with the PongService. That's a lot of code duplication that's scattered throughout the entire ecosystem that each team would need to manage.
And there is.. with Contracts!
Contracts and the tooling that support them go a long way in solving the aforementioned problems. Lets take the same scenario as described above, but walk through it leveraging contracts.
The first thing we need to do is define the contract itself. This can be in any format: JSON, YAML, or in this case, Protocol Buffers. Generally the format that you decide on will be in the format expected by the tooling and technologies you intend to leverage. The key take away here is that you define upfront how messages should be structured and the types of each.
This is a .proto file that explicitly defines how to interact with the PongService. The .proto file is the contract.
Messages are sent to the service using a PongRequest which has a message field, and then messages are received using a PongResponse which also has a message field. There is one endpoint that the service exposes, Get.
Ok, so you might be thinking.. "Cool? Looks a lot like API documentation to me. I'll go start writing some code that structures messages this way."
The beauty of leveraging contracts is that because there is an agreed upon way to interact with the service, it's really easy to automatically generate all of the concerns we spoke about in the previous example.
From the contract, we can automatically generate the PongRequest and PongResponse objects, there is no need for a developer to ever create them. We can automatically generate a Client so that you can connect to the service easily. We can automatically generate fakes, stubs, and mocks for testing. Everything you need to interact with the service can be yours at the press of a few buttons.
This moves the validation and enforcement of the contract to the left, way earlier in the development lifecycle. Remember in the previous example, we didn't really know if things were going to work out until the very end. The code generated from the contract gives us compile time confidence that the data is going to be sent and received a specific way.
We can even take this a step further, and from the same .proto file (contract), generate API documentation
The beauty of all of this is that if you were to change the contract, all of the resources that were generated above would be automatically regenerated during the CI/CD process. No more going into source code and updating client models or firing up some markdown editor to update your API documentation.
Contracts also makes it a lot easier to manage versioning and assess the ramifications of changing the contract when things do inevitably need to change. We can be notified on our local machines if a change that we are introducing breaks the currently defined contract. There's no need to ask ourselves "Is this a breaking change?". We can let the contract validation handle that for us.
Summary and Takeaways
This is an incredibly large topic as it can be applied to almost anything. This post really just scratches the surface. Hopefully, however, it was enough to introduce the idea of contracts and the benefits they can bring when you have a single, unwavering, source of truth that your ecosystem abides by.
Contracts enable the use of code generation with confidence. Why is that important?
- Code generation is your friend. There's no need to write your own client libraries in order to interact with a service.
- Code generation is your friend. There's no need to write your own API documentation.
- Code generation is your friend. There's no need to write your own testing stubs.