One of the often-cited concerns with leveraging reusable software is design complexity. This is indeed a legitimate concern and as designers, we ought to ensure that it is managed appropriately. In this post, I want to provide some strategies for tackling integration complexity:
- Build Iteratively: this is undoubtedly an effective way to avoid over-engineered assets. Building assets iteratively means realizing functionality in small bites, over multiple releases even. Instead of trying to implement a perfect reusable asset, prefer building in increments. This has several benefits: reduced risk, increased relevance for your applications, early feedback on whether the asset has captured domain relationships appropriately, and opportunities to remove code or refactor behavior on a continuous basis.
- Capture natural variations in the domain: reusable assets that don’t reflect the natural variations in the problem domain run into lots of issues. If you keep scratching your head trying to infer what the asset is trying to accomplish – examine the consumer-facing interfaces and ask yourself, does the interface reflect domain variations or is it providing needless variations or worse, ignoring must-have ones?
- Prefer convention over configuration: This is one of the foundational principles behind why frameworks such as Ruby on Rails are so popular. You can use this idea and simplify assets in many ways! For example, instead of forcing configuration for files, maybe a standard location would suffice. This idea can be leveraged with scripts that setup developer environments, automated regression tests, and reading/saving program output etc. There might also be cases where input data is used to determine class instantiation or stylesheet selection. Again, if you come up with a simple convention, many lines of configuration can be eliminated.
- Loosely Couple Capabilities: Loosely coupled capabilities are easier to change and integrate. By creating reusable assets in a loosely coupled manner, you will also make it beneficial for consumers. Loose coupling provides another important benefit – making it easy to isolate assets and test them as individual components. If you are building service capabilities, explore the use of asynchronous message publications for real-time notifications to data/status updates.
- Strive for consistent naming and behavior: consistent naming reduces learning curve for developers as well as makes it easy for the asset provider to debug, integrate, and test reusable assets. Consistent behavior should go beyond simple method calls – you can extend this to services and business processes as well.
- Make Assumptions Explicit: A lot of design complexity can arise due to incorrect assumptions – for instance, there may be operating assumptions about security, data integration, tracking, and error handling. There are a lot of design assumptions that get made as a natural part of the development process (e.g. every customer will always have a address, or that every customer needs to get authenticated prior to instantiating a business process). Make sure these assumptions are put in the open and for everyone to validate. It often turns out that an asset doesn’t have to implement a feature or that it may be implement an existing feature incorrectly.
- Provide consumer-friendly interfaces: Start designing from the consumer’s standpoint and strive for simple yet functionally rich interfaces. This has several benefits: you won’t expose needless internal complexity associated with the asset to the consumer (i.e. achieve right level of encapsulation) and also make it simple for consumers to integrate with the asset. If you have 10 options for a reusable asset but most customers use 2 frequently, why not set the other parameters with sensible defaults? Consumer friendly interfaces also ensure that you build assets that have tangible business value.
- Avoid abstractions for the sake of technical elegance: not every abstraction is meaningful, especially with respect to your problem domain. I wrote earlier about the domain-specific nature of variations and why one set of abstractions isn’t always appropriate for your problem. Experiment and iterate to get the right abstractions – they will help establish a shared language within the team and reduce needless complexity because of overly generic interfaces.
- Minimize compile-time and runtime dependencies: Reducing the number of moving parts – both in terms of compile time libraries and runtime libraries, services, and systems will make it easier to manage design complexity. Always, ask yourself – is this dependency absolutely essential? Does it introduce a single point of failure in the overall architecture? Is there a way to cache frequently accessed data or return that isn’t 100% up to date?
- Provide Mock Interfaces: When possible provide mock data/objects that can help consumers integrate and test assets quickly. This is related to the earlier point about minimizing dependencies but is also useful for customers to get a flavor for the kind of data or business rules that get executed as part of the asset’s functionality. Mocking also helps with another key benefit: asset co-creation. If you are developing in parallel with a consumer, mocking is a great way to agree on interfaces and develop in parallel.
What is your view on these strategies? Can you share some of the ideas/approaches that you have pursued to tackle integration complexity?