1. The Architecture Function Is the Departmental Glue
1.1. The Self-Contained Team Concept Takes You Only So Far
The ITIL® 4 Foundation manual posits that adopting ITIL without Agile risks an organization losing focus on value and creating slow moving, centralised bureaucracies, whereas adopting Agile without ITIL can lead to higher costs over time, such as the costs of adopting different architectures and of releasing, operating, and maintaining software increments.1 Whether you adopt ITIL or not, they have a solid point about ensuring a balance between higher level management functions and agility-enabling practices.
The Agile concept of the self-contained Scrum team is inviting because it presupposes that each team is capable of delivering an independent value stream, not being as hamstrung by crossing dependencies, and making the delivery of value seem more simple, manageable and predictable.
There are times during the evolution of a product where this can indeed be true, but in my experience this period is short lived; a project reaches a point at which this paradigm, taken strictly as defined, becomes more problematic than freeing. It is a reality of complex software systems that dependencies in its design are necessary – both horizontal and vertical – and a team management approach that doesn’t reflect this will cause the creation of a fractured system that will accumulate technical debt at a high rate and require refactoring early in its lifetime. A somewhat bureaucratic architecture function needs to be incorporated in a sensible way with the more dynamic, nimble Agile practice in a way that allows both functions to succeed and create a balance of platform functionality and platform health, emphasising extensibility, scalability, performance, and security. I’ll cover an architectural management paradigm that speaks to this need later in this essay.
Now, I don’t want to be seen to be bashing the self-contained team concept. Once the architectural underpinnings of your teams’ work are established, nice service design can allow your teams to work largely independently of each other within their verticals, helping you build concurrency and overall higher velocity. This is certainly a goal you should aspire to.
1.2. Skilled Architectural Oversight Prevents “Proof of Concept as a Service”
Generally, any engineer junior or senior can pick up an appropriate full-stack framework, define a data model, implement business logic using the engineering patterns defined for that framework, follow some best practices, and stand up a working application. In the case of a SaaS web application, for example, they may stand up a front end, one or more back-end services, some data stores, and some identity management apparatus. With relatively little effort, a working web application can be put together that ostensibly demonstrates a given starting minimum proposition of business value. On the surface, this is important for the business because it builds trust that they can get something engineered to start realizing their vision.
There is an insidious problem lurking though. The documented standard patterns for many frameworks almost encourage naïve architectures; they’re able to solve a specific problem well (such as presenting, interacting with, and persisting data) but when it needs to support a massively scaled system, the assumptions on which a framework is built can quickly become limiting. There are certain kinds of challenges common to larger, scaled architectures that many full-stack frameworks are not intended to solve. This proof of concept is almost certainly not an enterprise-grade application as this point.
This problem is unfortunately very pervasive when engaging outsourced teams providing you commodity skills. Only early, skilled architectural oversight that knows what it’s looking for, and that knows how to solve these challenges in the right way and at the right time using enterprise-level architectural patterns can prevent a business from taking a proof of concept into production. This is most certainly not a commodity skill. Framework patterns implemented by engineers must be overlaid on the enterprise architectural patterns defined by an architect.
1.3. What Could Possibly Go Wrong?
Yes, I mean this in both the sarcastic and literal senses. I’ve intentionally spoken only in generalities so far, but because it’s important to have a strong sense of exactly what kinds of things can go wrong and the pain they can inflict on the business, I’ve provided a sampling of some of the most common ones I’ve experienced. I aim to emphasize the impact of various issues more than their technical underpinnings. In their own way these are all real risks, and they need to be actively managed over the life of a platform:
- Unregulated database schema changes, such as new tables or columns with missing indexes, suboptimal joins between tables, improper versioning of database objects, and code getting out of sync with the database schema. These can cause massive performance problems at the component or system-wide level and runtime errors. These could necessitate messy deployment rollbacks and service windows.
- Accumulation of technical debt in an unregulated fashion, with no way to prioritize its remediation, or what is considered acceptable for now. This creates risks that are not managed.
- Inconsistency in build process and deployment strategy between components and environments. This makes release automation more challenging, and also makes it more difficult to know whether a deployment was completed successfully.
- Differences in API paradigms and payload structure, making addition of monitoring and alerting more difficult for SRE, meaning issues in the live system may not be detected in-house, but may be impacting customers.
- Zero governance, making passing a SOC2, ISO, GDPR, SOX, SSAE 16, NIST, FIPS, CCM (name your flavour) compliance assessment very hard. If you cannot prove compliance, this can limit the clients you can get onto your platform, especially in the enterprise client realm.
- Sprawl in provisioning of compute assets that might not consider optimal use patterns, making hosting cost management difficult, and dramatically increasing hosting costs.
- Lack of complete architectural view of the system from both the compute/storage/network layout or platform component layout, creating risk in security posture as infrastructure/SRE may not know what to secure. This also makes knowledge management across the teams difficult.
- Monolithic components of the system that are too tightly coupled internally, or that have too much responsibility, commonly causing quality, extensibility, and performance issues.
- Components such as microservices that are too tightly coupled between each other, commonly causing extensibility limitations.
- Sharing of common databases and database objects across service domains (verticals) commonly causing performance bottlenecks, extensibility lock-in due to use of common artifacts that make them hard to change independently, and fragility due to changes in assumptions surrounding those artifacts.
- Creation of overly configuration-driven systems causing high complexity, affecting the ability of engineers to comprehend the platform and code without causing bugs because it’s a less deterministic system, and requiring highly skilled staff to configure it.
- On the other hand, creation of components that lack a vision of extensibility, requiring heavier refactors in order to add functionality to them, thus limiting business flexibility and timelines.
1.4. Architecture and the “ilities”
Of course, architecture isn’t just about avoiding mistakes in design, but also about providing for those non-functional qualities of a software platform that drive success at scale, and scale is what the business needs. These are attributes you don’t get for free. As demand on a system grows, the identification and reduction of bottlenecks in both platform performance and development practice alike become critical. The ability to produce and extend quality software is highly tied to cleanliness of design. Software that is better designed is inherently more testable. Software that is designed correctly along data domain lines stands a chance of being more performant and extensible. All these attributes are intricately connected. If you’re missing one, you’re probably suffering elsewhere too. I’m not going to go deeper into the ilities just for the sake of it (I’m not trying to create a definitive work on the topic here after all) but I point these out to make evident the broad scope of the architecture function, and thus to help influence the way in which this function is seen, funded, and managed.
Now I’m also not here to preach a specific architectural paradigm or go into its details. No matter what the paradigm, it is imperative that from the outset, strategies for the attainment of these attributes are considered by architecture, and that the business knows that there will be times during the life of the platform where real effort needs to be invested to keep the platform on pace with its demand. They don’t all need to be implemented at the start – in fact far from it – but the transition to being able to support it when it is time be made much smoother if the design doesn’t veer off in an unsupportable direction right out of the gate (I have a whole section on Just-In-Time architecture below). This is a key reason to engage a senior architect very early on in the lifetime of the platform.
It is tempting and common for a product-focused leadership team to focus on functionality only, yet the ilities are so critical to the success of a platform that the architect must be on the hook to senior leadership to ensure these are being actively managed from the outset. The architect should be required to present a strategy to leadership that demonstrates the way in which these are being contemplated. If these cannot be formed into a cohesive conversation that considers resourcing and architectural roadmap at a level the leadership can understand, you may have a problem. If you don’t advocate for it, leadership may wrongly assume they’re getting these built in.
In conclusion of this section, your Architecture function is the glue because it brings advanced software & infrastructure design and implementation thinking to a platform; it brings everything together, with the specific aim of providing the ilities and avoiding “Proof of Concept as a Service”.
1.5. Practical Considerations for Managing the Architecture Function
1.5.1. Employ a Non-Coding Architecture Leader
There is a surprisingly common belief that an architect will lose effectiveness if they do not stay close to the code, and that requirement is often codified in their job description (this is often a requirement of the CTO role, too). During the earliest days of a platform’s development, this may be a fair assertion to hold true for a time, as it is critical that it gets off to the right start, but only to the extent required to make sure it does. In reality though, architects with this kind of hands-on responsibility cannot scale.
Before long, it’s for all the aforementioned reasons in the previous section that if your architect is coding, you are almost certainly exposed to more unmanaged risks sprint over sprint; the larger a system gets, the less bandwidth they’ll have to consider these risks let alone lead solutioning for them. Add on top of this the responsibilities of proactively designing for upcoming product requirements (more about this in the Technology and Product as Partner Orgs section), and any governance activities across teams, and they will become quickly overwhelmed and less effective at every aspect of their job.
Moreover, if you have chosen an architect who actually wants to code, they may not have the enterprise experience to manage these risks to start with, as enterprise architects are built differently and enjoy the larger challenges that come with that title. You’ll have longer term success by hiring a non-coding architect and equipping them with solid senior engineers to help design and implement the vision.
1.5.2. Employ Just-In-Time (JIT) Architecture
No business owner wants to be told that 6 months of up-front architecture work will be required before seeing any business value at all. The amount of time is difficult for a business owner to comprehend intuitively, and for that matter to have faith in the estimate to start with. They have no way of knowing whether it is a fair number or completely underestimated. All they feel is risk in an area whose nature and management are not native to them, and no owner wants to be in that position of feeling powerless, especially when investors’ money is involved to get you bootstrapped.
On the other hand, we’ve already discussed that an enterprise-grade platform cannot be brought to life without the thought and design that reflects the horizontal and vertical dependencies a large system will need. A middle-ground approach is needed that allows the architecture to develop incrementally while enabling value to be delivered, and that enables leadership to develop faith in both the ability of the team to deliver business value, but also that it’s being created in a way considerate of the “ilities”. This also creates a more engaging, ongoing conversation with leadership which can be educational for non-technical leaders, helping them better understand the risks and considerations in play.
JIT architecture is about creating just the right amount of architecture for the point of evolution a platform is at or needs to get to next, and not doing any more than is necessary lest it stall execution of new functionality for too long. JIT architecture is a design approach that recognizes not every architectural challenge needs to be solved out of the gate because either functionality does not yet require a certain architectural capability, or that certain of the “ilities” are not required for some time down the road that it requires a significant new architectural capability to support them now.
To make this less abstract I’ll provide an example – let’s momentarily contemplate a greenfield service-oriented (SOA) system built using microservices. One of the necessities of designing an event-driven SOA system is the need for a messaging or event stream framework that can be used by services to publicise that they have taken a certain action, usually changing state in the system. This prevents a tight-coupling between services in that each service does not need to be aware of the specifics of how an event takes place – a consumer listening for that event just needs to know how to get the data it needs when it has taken place, and the publisher of the event does not need to know who cares2. This is a piece so fundamental to the SOA pattern that it should not be deferred; it is much harder to retrofit an existing system that has tightly coupled services that has already reached its reasonable extensibility limits. In a JIT architecture, this should be one of the first things considered.
A potentially deferrable architectural task, on the other hand, is the approach a system might take for increasing throughput from its databases in the interests of performance and scalability. One such approach is sharding – the practice of using some arbitrary token such as the last number of a user’s internal system identifier, for example – to cause the system’s data layer to retrieve and persist a user’s data into a different database instance. A system with 2 shards might store even users’ data in one instance, and odd in another. This approach effectively halves the apparent database IO limits on the system, meaning the system can handle significantly more load before the database becomes a bottleneck (all else being equal). The reason this task can be potentially deferred is that the approach to data access is usually hidden behind an abstraction layer, and as the platform continues to evolve this approach can be changed behind the scenes with no impact to the service-layer code base.3
1.5.3. Architecture and the Self-Contained Team Concept
I’ve seen scrum teams wrestle with how to integrate the architecture function time and time again. Pure Scrum asks the Product Owner to specify user experience, and the Scrum Master mediates the back-and-forth between the PO and engineers to ensure the need is entirely understood. The estimation sessions follow, and the engineers go to work. However, if we insist that a team isn’t necessarily allowed to just proceed alone when there are actual or potential side-effects, the team can get stuck because now it is not entirely up to them to estimate the whole scope of work required, architecturally speaking, to get the job done. This will disrupt the flow of scrum and when poorly done, in a multi-team environment the architecture function can quickly become a bottleneck, causing the whole development engine to back up.
This does not mean, however, that the architecture function should be deprioritized in the interests of functional velocity – that would be a strategic mistake of the highest order and would be a textbook case of mortgaging the future for the now. In a multi-team environment, you must create an authority inside your department to manage the multitude of dimensions of platform development and maintenance.
Moreover, as upcoming functional needs approach from roadmap, there can often be a cross-over between functional areas in that data from more than one vertical may be required to satisfy the need, or even brand-new components of the architecture (microservices for example) may be required that don’t rest with any existing team yet. There needs to be a centralized coordination of designs and development activities to get this done right. We need a collective of architectural thinkers, development process thinkers, and vertical subject matter experts from across the platform – we need an Architecture Committee.
1.5.4. Create an Architecture Committee (AC) – Architecture as a Service
In fact, the architecture function can be integrated with Scrum, as long as you’re smart about resource usage and timing of architecture activities, you have experienced team leads, and you have laser focus on the architecture function – most especially where its responsibilities end and the teams’ responsibilities begin. I’ll begin by describing the responsibilities of the AC as I see them:
- Approve database changes with an eye to name and data type conventions, foreign key management, index management, and understanding usage patterns, to name a few
- Maintain the Technical Debt Register and collaborate on its prioritization in line with business goals as agreed with leadership
- Maintain platform architecture diagrams, including component-level and infrastructure-level diagrams
- Oversee and report on the implementation of code quality initiatives, correcting and recognizing engineers as appropriate
- Discuss the upcoming feature pipeline as determined by the Architect in their cooperation with the Product function in order to drive any new components or architectural capabilities required before the scrum team gets to it so the AC isn’t a bottleneck (more on that shortly)
- Planning infrastructure expansion or consolidation including compute, storage, and networking configuration needs using feedback from the platforms’ performance from the SRE team
- Collaborating to determine the split-points of microservices that have become too large (“microliths”, to coin a phrase) and have too much responsibility, such that new smaller microservices result .
- Set standards for common concerns to be used across teams, including standardizing on library usage, coding patterns, and the like.
- Cross-pollenate knowledge between the developers, DevOps, SRE, and Infrastructure. For example, the SRE team needs to know what to put monitoring and alerting on, and the developers need to know how the application is performing in the wild.
The members of the AC should be:
- The Chief/Head Architect and any subordinate architects
- Team leads from each vertical, horizontal, and practice area as appropriate to your architecture and department structure
- Representatives from DevOps, SRE, and Infrastructure
1.5.5. Timing the Activities of the Architecture Committee
The three main keys to making an AC work and integrating naturally into Scrum are early and frequent engagement between Product and Architecture, engagement of the AC ahead of any functional or architectural requirements well before coding would be due start, and the formalized allocation of time for team leads and senior engineers in their sprint schedule to attend committee meetings and consider the committee oversees. I have a whole section on the partnering of Technology and Product so I’ll not discuss the Architecture engagement further here except to say that it is imperative that Architecture understands every aspect of what is required by roadmap well before it is brought to the AC. It sure helps if your Architect isn’t involved in the day-to-day operations of the department so they can remain strategic, too.
Now – engineers need time to think. It is unreasonable and suboptimal to ask senior engineers to turn around a design candidate within the course of a few days when they also have responsibilities for execution too, and certainly not within the timebox of an AC meeting; you could get a bad design if they feel rushed, and this exercise should not be completed in just one meeting anyway. A potential timeline for this series of interactions follows:
- The Architect should present their initial challenge a solid 3-4 sprints ahead of when any execution might be expected to start. During this meeting the architect should lay out the functional or architectural requirement, and any conversation around the most relevant “ilities” that concerns them should take place now. Unless the solution is entirely self-evident, solutioning should not be required during this meeting, or even outright avoided. This should be an opportunity for the AC to question the Architect to ensure they understand the scope of the ask. It might also be a time to work out who will take responsibility for various aspects of the creation of a candidate design. The goal here is to make the engineers think about the design themselves, not to have the Architect push the design down to them, otherwise you’ll discourage growth and a sense of ownership. You also don’t want to burden the Architect with low-level detail.
- Let the team stew on it for at least a week. During this time, they might collaborate as a team to create a candidate design they can back, and might come back to the Architect with any questions needed in clarification. Even if they finish the design candidate early, there can be a lot to gain by letting them sit on it for an extra few days; I’ve had many a game changing epiphany that one of my assumptions or assertions was incorrect when I wasn’t even thinking about it. You want time for those to bubble to the surface – they’re part of the creative process and they’re de-risking. In short, don’t rush them.
- They should document their design in a central repository (Confluence or the like).
- Once this period has elapsed, the individuals or team should present their candidate design to the Architect. This should be a time for the Architect to quiz them on every aspect of their design candidate, and to ensure they have given adequate consideration to the ilities. The Architect may send the team back to tweak their design and reconvene again later. The AC should now be positioned to create a preliminary sprint map to execute the initiative iteratively.
- The Architect should now discuss resourcing considerations with the department’s resource manager(s). At this point the initiative would be scheduled into the resource plan as any other.
1.5.6. Allocating Time for Architecture Committee Activities
The time a senior engineer needs to spend in AC activities (meetings and thinking time alike) should be budgeted into their sprint. I’ve always encouraged my leads to spend a whole day or two on this activity, front-loaded in the sprint, so they have time to stew on their designs, and so they don’t need to context switch between coding and design work. Help them to stay focused. In fact, insist upon it – not everyone knows how to manage their time optimally. Don’t do this towards the end of the sprint as they’ll be under pressure to finish deliverables.
AC time should not be extra to their coding and team lead responsibilities (reviews, demo preparation, and the like). As a matter of routine, their responsibility in the AC should not require out of hours work. Senior engineers are more sensitive to how their time is used and unless they’re a machine, they’ll quickly get out of balance and come to resent their job if this is considered extra-curricular. Put explicitly, this means your best engineers will not be coding as much as your junior ones, but it’s the way it should be.
Allow me to explain how I justify my position on this. In my time in the Navy as a bridge officer, I was always being trained by the officers who were trying to get elevated up and out of the job I was training for. If the Navigation Officers were not performing to an adequate level to gain the Commanding Officer’s trust in standing a bridge watch unsupervised, my predecessors could not proceed up their own growth path; they would have to stay on the bridge until such time as their replacements were ready. Moreover, I was also training my assistant navigators so I could follow the same path. This is the circle of life, and the promise of progressing up the leadership chain toward Command drives mostly every bridge officer. Of course, this happens over years, but this is a continual, actively managed activity. The sense of team cohesion it creates is something I miss as a civilian, and I see it as an oft-missed opportunity in the corporate world.
Although your seniors might be your best engineers, their job can’t be to just code. Their job is to help bring their junior engineers up a level, to elevate everyone’s thinking, and to play their part in elevating the platform. You want a team of engineers who are aspiring to growth, not just churning out code. Yes, of course their coding is important too, but it needs to be a different balance for them.
1.5.7. Don’t Waste Time with Multiple Formal Design Proposals
If the department is in a steady-state cadence of design and execution, the goal should be that the Architect’s leadership of the AC results in the optimal forward-looking solution, and that leadership should trust that this is the one to take. This is a goal, not a blanket statement. Some Architects have a tendency toward over-engineering, not everything runs smoothly all the time, and past slip-ups may justify caution on the part of the business. Optimally though, a proven architecture team can deliver at this level of function and the technical executive should try to stay out of the detail. If there is a distrust this certainly needs to be worked out, in which case prefer to observe design discussions (in the AC) over requiring them to document multiple design options. If you do this you may get a sense of where the dysfunction is – be it personalities, attention to detail, technical skill, or whatever, and you could start along a road to fixing it. Don’t waste their time asking them to document options they know they’ll not execute upon as your de-risking strategy.
1.5.8. Sometimes You Have to Hack Your Way Out
To reiterate, the above applies to business-as-usual engineering efforts. Sometimes though, major requirements pop up when not expected and when least convenient. The executives are in a pickle and need technical options to address it, to de-risk the situation, and to take the power into their own hands to make the implementation choice they’re the least uncomfortable with – even (or especially) if they’re not technical. This is fair and totally within their purview. I don’t know any executives, technical or otherwise (myself certainly included) who haven’t been in this position at one point or another.
It's not ideal but the choice really comes down to one of a couple of approaches – the shorter but nastier and the longer but healthier. The timeframe of the longer approach may be untenable to the business, and the shorter approach will generally cause limitations and accumulation of technical debt you’ll need to spend time with down the road. Either way, unless it is obvious that each one will take roughly the same amount of time, don’t waste anyone’s time documenting the longer, more correct one. You already know you’re unlikely to take it. Instead, document the nasty one, and have the Architect place particular emphasis on what risks you’re creating by taking it. If it’s more risk than you’ve appetite for, go all-in on a more forward-looking option. If you do take the nasty option, put the considerations on the Technical Debt Register and get back to them as soon as makes sense.
Another unpleasant factor you might consider is that depending on the personalities in your team, it’s not unheard of for there to be a deliberate bias against the nasty option and for them to overplay how bad it really is. Now it’s one thing if there is legitimately no quick way out of the jam – the engineers should push back. It’s another however for them to be inflexible and unempathetic and offer you no way out. Sometimes you just have to go with your gut on this, and that’s made easier if you know your team and your architecture.
1.5.9. The Code Review – Whether and How to Conduct Them
The choice of whether or not to include code reviews into a Scrum development process can be a divisive one. On the one hand, it is a highly advocated activity because it is seen as a large risk mitigator. Even though it’s conducted at the 11th hour it is ostensibly meant to ensure that no badly structured or buggy code will be committed. I’m not in this camp.
On the other hand, it is an activity that is seen as a hassle by some engineers that interrupts their own work, can be hastily conducted and not detect the issues that are most relevant, and very often reviews will get backed up in a senior developers’ queue until they’ve finished their own work, thus blocking the merge of commits until a time later in the sprint. In my experience, these reviews have as often as not even stopped problematic code, and I would often see multiple follow-up commits to “approved” code. It takes a particularly conscientious engineer a lot of time to be good at these, and that’s not how I want them spending their time.
For me as a CTO, more than any of the above many reasons for my taking this stance, certainly the most important reason was that delaying merges causes the delay of integration in the lower test environments, and thus the deferral of automated tests. By the time the changes get merged and can integrate it may be toward the very end of the sprint before the tests run and that it is only then realized that there are issues. Now – suddenly – you have engineers working late to remedy the issue. I believe the risks of not conducting code reviews are largely mitigatable, and easily so at that. Code reviews in their traditional form are not the best way to manage risk.
I’m going to refer back to my earlier assertion that senior engineers/team leads should be highly engaged in the design of the work their whole team will be doing. Understanding the intended design, taking the wrong approach can largely be averted if they simply frequently peer over their work mates’ shoulders earlier in the sprint (I mean this figuratively, video calls work fine too) to ensure they’re implementing the design. This is made easier if the engineers are taught to execute with a top-down approach to their coding instead of a bottom-up approach; if a bottom-up approach is used the structure of their work may not become apparent until later, thus making it necessary to “check back” later to see what they ended up doing, again late-gaming the safety check you’re going for.
This early-on effort comes with the expectation that the senior engineers have the time budgeted to do so. I’ve had success asking them to conduct these in the morning to get them out of the way so that they themselves can focus on their own work for the balance of the day. This doesn’t need to be a hard-and-fast rule, but many engineers complain of distractions so it can be a helpful guideline.
I cannot completely stand by this assertion unless we incorporate another critical piece, however, and that is CodeScene (which I discussed in an earlier section on bugs and code quality). We still need to assure good code quality and make recommendations on fixes. It is hard for a tool to make recommendations on appropriateness of design, so it is still important to fully understand the design and check in early and often to make sure the developers are heading in the right direction. However, a tool like CodeScene can detect issues such as lack of domain language, primitive obsession and the like that can inform a weaker design. With a tool that can do this and that cares about code organization, you have most of your code review bases covered.
Lastly, there is another important piece in that code reviews are often conducted with an eye to making sure the functionality – as coded – would work as expected. An expectation that code reviews can reliably detect these is a folly, and this is exactly what automated tests are for. Don’t have your senior guys trying to nut this out for themselves; make the junior developers and SDETs write tests instead.
In short:
- Senior engineers must deeply understand the design and they must conceive it
- Senior engineers check in early and often with junior engineers as they implement
- Junior engineers take a top-down approach to their implementation to make structure apparent earlier in the sprint, thus intercepting incongruent implementation early
- Use CodeScene to automatically review every commit for code quality issues that humans typically gloss over but that will become a locus for bugs if left unchecked
- Use CodeScene at the architecture level to ensure component complexity isn’t increasing in ways that are unintended
Next Up
Although having a well architected, performant, scalable, extensible quality system is an ultimate goal, none of these matter if your company isn't alive anymore because your platform and infrastructure was porous and suffered a ransomware attack or a disaster from which you could not recover. I'll cover some of my previous career revelations in my next post.
You can view my next post titled "Platform Security and Sleeping at Night" here.
No comments:
Post a Comment