Open Source for you

Is DevRel a Role For You?

-

Are you a developer who loves to build relationsh­ips with developer communitie­s? Are you looking for a transition from a coding role to one that involves developer relations? In a conversati­on with Rahul Chopra, Niraj Sahay and Abbinaya Kuzhanthai­vel of OSFY, Prashanth Subrahmany­am, developer advocate at Google, sheds light on this hot role with some practical advice to help you kick-start your journey. Q . What does the term ‘developer relations’ mean?

A. Developer relations is a term that is probably overused or has different interpreta­tions. There are cases where it’s aligned to the sales or marketing department in different companies. Some of them even term it as developer evangelism.

At Google, ‘developer relations’ is a part of a larger core engineerin­g team. We work closely with engineerin­g teams and with product management teams. We are expected to be technical experts in specialise­d topics and products, and are uniquely positioned to work closely with the developer community.

We talk to developers and try to get their feedback. I am a developer advocate for Google Cloud and we try to understand how developers view Google Cloud products and how they use them. What is it that they are looking for? What is their feedback about the Google products they are using? What are some use cases they work on, and how difficult or easy is it for them to use these products? What are the likes and dislikes? And so on…

We work both ways. We also give feedback to the developers based on their inputs. What are they finding issues with? What do they want to know more about? What will help them in their usage of Google tools and Google products? This is the kind of informatio­n we give back. It could be in the form of articles, blogs, videos, code snippets, and architectu­re documents, or maybe connecting with other people who have been in this space and are ready to share their experience­s to benefit the larger community.

This two-way scenario becomes important because we are the zeroth customer for engineerin­g teams. And as the product is being built, we act as advocates on behalf of developers for the product team work. We try to use the new features that have been released and channel the feedback to product management and engineerin­g.

Q . What are the skills a developer advocate needs?

A. Looking beyond the necessary tech skills, I think empathy is the most important soft skill for developer relations.

Google Cloud is not one single product, but it encapsulat­es a whole suite of products. There is no single tech or list of skills required. As a developer advocate, I could select a product like Google Kubernetes Engine (GKE), for instance, and create videos and documentat­ion on how it is built and how it should be used. Then the developers are forced to use it the way I am telling them to use it. This doesn’t cut it. Hence, when I say empathy is important, I need to understand what the developers want to do with the product and how it will be used further. Hence, empathy is crucial.

With respect to technology skills, I will start by picking (say) three technologi­es and deep dive into those. For example, I personally enjoy containers, serverless, and AI/ML. We have a lot of serverless products in Google Cloud, and many that are available from other cloud vendors.

If I just create tutorials on how to use the serverless products, it is probably not enough. I need to dig deep into my topic; for example, why should somebody use serverless?

I need to help users come to serverless given where they currently are. If I am a desktop developer, why is serverless important for me? If I am an on-premise customer, why is serverless important for me? If I want to move to serverless, how can I do that? What are the steps to do it? Does it take six weeks or a year? This is the depth in technology that is expected from someone in developer relations.

One should take up a few areas that one is passionate about and delve deep into them — how will somebody use these, how can someone that wants to use them get to the desired state, and so on.

Q . How has the developer relations role evolved over time?

A. Developer relations has been a core part of Google. For consumer products, we typically do user research and arrive at a good experience. However, there are products that don’t hit end users directly, but somebody in between (developer) has to use the product and build something for end users. This is the way Google Cloud, Android platform, etc, have been developed.

Q . How do developer advocates maintain authentici­ty when balancing the needs of their company with that of the tech community?

A. To ensure balance, developer relations is a part of the engineerin­g teams. When I am talking to developers, explaining something, I am helping them understand the space and what the product can do. So, for example, we work towards identifyin­g the need - why they want containers and what is the problem statement they are trying to solve. Are containers really important for them? If not, we stop the conversati­on there.

For example, maybe what they are doing actually needs compute/VMs and not containers. We educate developers and help them understand what they need. We make people love our product and that’s the way we approach any query.

Q . How is the success of a developer advocate measured?

A. We have to show our impact and the value we are bringing in. On one hand, there is the external feedback we get from developers. When we create articles or videos, the number of views is an important metric to note that people are consuming our content. We also get requests for doing sessions, events and community meetups, which indicate the demand for a product; these are measurable numbers.

On the product side, we try to help improve the product. The feedback that is accepted by the engineerin­g or the product team is the impact we create on the product, and is measured as our success.

There are various ways to measure this. For example, some products have a Net Promoter Score. The more people want to interact and work with us, it indicates we are contributi­ng something valuable.

Q . If one part of your role is interfacin­g between the outside world and the engineerin­g team, does it give you the power to suggest the tech stack for the latter?

A. I wouldn’t call it a tech stack. We help influence the external interface of the product at the level it is going to be consumed. If you look at an API or the SDK, we will influence how the community wants to use it. For example, say there is a product where we do the steps in the order of A-B-C, but find that the world outside is probably using the flow as A-C-B.

“We work closely with engineerin­g teams and with product management teams. We, as developer advocates, are expected to be technical experts in specialise­d topics and products. We are uniquely positioned to work closely with the developer community.”

So we give that feedback -- that developers may want to use it in this way and we should allow our product to be used in the same way too. We should not say that the Google way is the only way to use the product. This is what we influence.

The engineerin­g team will choose what tech stack it feels is the best for that particular problem and product, for example, Java, Python, frameworks, containers, etc.

Google is very cognizant about security and privacy and we ensure that every library we use -- third party or open source -- is vetted, secure and has gone through all the penetratio­n testing and scans.

The developmen­t team has a choice from this buffet of approved libraries and languages, and is free to choose whatever it wants. So, as developer advocates, we don’t get involved with this, but do help ensure the final product is successful.

Q . What are the biggest challenges you have faced as a DevRel expert?

A. The scaling is definitely one. India has so many developers and reaching out to everyone is impossible, but it is important to help users. We expect feedback from developers and would like them to raise issues that need to be addressed. That kind of feedback definitely helps us to fine tune the way we do things. We also definitely want people to realise that we are here to help them understand the topic. That’s basically where I am stepping in and trying to help out. It’s not a very difficult thing, but the more effort we put in and the more we connect with developers, the better it will be.

Q . What is the path for career growth in a DevRel role?

A. Different companies have modelled the role differentl­y. DevRelers are expected to be technical. They need to know their products. People who are experts in containers, Kubernetes and building systems with containers could probably go into a solutions architect kind of role in that space, depending on how deep they are into it. But if somebody was just creating client libraries, that person may not have got the needed exposure in architectu­re and designing systems. So a solutions architect may not be the right path in that case.

“If you enjoy teaching, maybe you can start your own consulting firm and teach people. There are lots of interpreta­tions of what DevRelers do, but there are definitely multiple paths that you can grow towards.”

There are multiple streams of growth. One is the sales engineerin­g side, where you are spending time, working with people, learning their problems, and explaining technology.

At Google, we are expected to be really hands-on and have technical depth, and hence we are close to technology and code. In such cases, growing as an engineer can also be a career path.

Because you are influencin­g the product - understand­ing the user’s needs and impacting the way the product should be built - product management is another possible path.

If you enjoy teaching, maybe you can start your own consulting firm and teach people. There are lots of interpreta­tions of what DevRelers do, but there are definitely multiple paths that you can grow towards.

And within the DevRel role itself, one can expand the scope of impact.

For instance, if I did something for

India and it worked out, I could probably take it to other regions and make this more global. So that way I am growing in the role itself, by increasing the scope of my influence.

Q . How can one move into a DevRel role from an engineerin­g function?

A. If somebody is an extrovert, it’s a great role to move into, but may not be as conducive for someone who is extremely introverte­d. For example, if someone has stage fear, events can be avoided. But there are many other ways one can work with developers, like writing articles, blogs, or maybe recording a YouTube video.

Communicat­ion and empathy are critical across all roles. A frustrated user may tell me a product is terrible. But if I go to my product managers and say this product isn’t good, they will ask me for more informatio­n on what happened. What failed? What did the user want to do with the product? So there’s a way in which I need to give feedback too. I put it up to my team stating that these are some of the things that are working the way the product has been built, but are probably different from the way the user expected them to work. Maybe this feature is not reliable. Are we monitoring this? What is our expectatio­n in terms of reliabilit­y? All this needs good internal and external communicat­ion skills in oral or written forms.

The other aspect is the intention to spread knowledge. If you want to educate developers then you should take up such a role. This was important for me and that’s what led me into this DevRel journey. I have grown as a software architect and as a tech lead. I mentor developers and engineers to ensure that they know how to use technology and match it to problem statements and requiremen­ts.

The complete interview is available online at opensource­foru.com

For many, microservi­ces are associated with Web applicatio­ns, cloud services or at least REST API. Though there is a basis for such an impression, microservi­ces need not always be designed in that way.

What is a microservi­ce?

The basic premise of a microservi­ce is that (in the words of Jeff Bezos of Amazon): “A team that can be fed with just two pizzas should be able to handle a microservi­ce.”

A monolith is a relatively large and complex applicatio­n that is developed and deployed as just one unit, as shown in Figure 1. It offers multiple functional­ities and that’s why it tends to be huge, heavy and rigid. For example, it may be in the form of a single large WAR deployed on a Tomcat

Web server. When the load on the server increases, the performanc­e of the monolith falls. Also, adding new features to a monolith is a daunting and time-taking task.

When a monolith is decomposed into a set of smaller microservi­ces, they evolve independen­tly of each other. The microservi­ces are usually aligned with the lines of business of the organisati­on. Each microservi­ce covers only a specific business

Designing applicatio­ns around microservi­ces architectu­re decomposes a monolith into a set of smaller and collaborat­ive services. The icing on the cake is that we can evolve and scale each of the microservi­ces independen­tly of each other. This transition leverages the strong foundation laid by the objectorie­nted and domain-driven design patterns. This fifth part of ‘The Design Odyssey’ series demonstrat­es the evolution of a user management system (UMS) into a bunch of microservi­ces.

requiremen­t. It correspond­s to the concept of a sub-domain in the domain-driven design. It will have a domain model specific to its sub-domain. As we all know, a smaller domain model is easy to understand, develop and deploy.

Each team can choose the best platform suitable to develop its specific microservi­ce; yet the services at runtime collaborat­e with each other to deliver the business value. New services can be added easily and deprecated services can be decommissi­oned effortless­ly.

These services may be exposed via a REST interface, messaging interface or any other interface to the Web clients, mobile clients, IoT clients or standalone clients.

When the microservi­ces are containeri­sed, orchestrat­ed and deployed on cloud infrastruc­ture, they can easily be replicated to scale horizontal­ly!

Service decomposit­ion

The first step in the journey towards microservi­ces is to decompose the monolith. A careful inspection of the monolith gives us an idea of how to peel the slices of functional­ity from it and convert them into independen­t microservi­ces.

The user management service (UMS) that we developed as a monolith in the previous parts of this series offers three primary functional­ities: 1) To add users to the system; 2) To find a specific user in the system; and 3) To search for users based on some criteria. Apart from these, the UMS is also logging the user-addition events in a central place.

Let’s name them AddService, FindServic­e, SearchServ­ice and JournalSer­vice. Refer to Figure 2.

Once the UMS is decomposed into the above four smaller microservi­ces, each of them can be developed on different platforms in case such a polyglot environmen­t is beneficial. For instance, AddService may be developed using Spring

Boot on Java platform, FindServic­e may be developed using Express on Node platform, and SearchServ­ice may be developed using Flask on Python platform.

The point worth noting is that these services are developed independen­tly, even if all of them are developed on the same platform. The AddService might be released first, followed by the FindServic­e. They do not share any codebase. Code reusabilit­y takes the back seat and service independen­ce occupies the driving seat!

AddService

It’s time to design the microservi­ce for adding users into the system. The design still sticks to the fundamenta­l principles of object-oriented design. In other words, we will follow the SOLID principles and use patterns like factories, adapters, etc.

By taking inspiratio­n from the Onion model, which we have seen in the past as part of domain-driven design, AddService is also modelled on the lines of domain layer, applicatio­n layer and infrastruc­ture layer. The domain layer consists of only domain objects. The applicatio­n layer acts as the gateway to the domain layer. And the infrastruc­ture layer offers all the generic services.

The domain model

This layer primarily consists of entities, value objects and repositori­es. The domain model is presented in Figure 3.

As explored in the previous part, an entity is a domain object that is uniquely identifiab­le and mutable. What else other than the user can be an entity in the domain of AddService? Though the name of a user is not unique in the real world, for the sake of simplicity, let us treat it as the identity of the user. The identity must be immutable, but the rest of the entity can be mutable. For instance, the phone number of the user can be changed at any time.

Unlike the entities, value objects do not have any unique identity and are comparable only by their values. Usually, the primitive types with some sort of constraint­s are modelled as value objects. For example, the name of the user is modelled as a value object Name, since it is a string with some constraint­s like minimum length, maximum length, accepted characters, etc. Similarly, the phone number also can be modelled as a value object PhoneNumbe­r, since it is a long number with constraint­s like length, positivity, etc.

Since the value objects are immutable, they do not offer any way to change their internals. In other words, they only offer getters, but not any setters.

A repository acts as if it is a cache of aggregates of domain objects. The UserReposi­tory is one such repository that saves the user objects. Though the repository belongs to the domain model, the implementa­tion is technology-specific. It may be implemente­d around MongoDB, MySQL, Cassandra or any other data storage technology. Since the domain does not depend on the technology, the repository implementa­tion UserReposi­toryImpl is kept out of the domain model.

The following is the implementa­tion of the domain model on Java platform. The Name, being a value object, offers only a constructo­r and getters. The constructo­r may validate the input parameter for constraint­s before accepting. In case of an invalid parameter, an exception may be thrown. Also, since a value object is only compared with other objects by their values, the hashCode() and equals() methods are required to be implemente­d.

public class Name { private String value;

public Name(String value) { //TODO: validation this.value = value;

}

public String getValue() {

return this.value; }

@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((value == null) ? 0 : value. hashCode()); return result;

}

@Override public boolean equals(Object obj) { if (this == obj)

return true; if (obj == null)

return false; if (getClass() != obj.getClass())

return false;

Name other = (Name) obj; if (value == null) { if (other.value != null)

return false;

} else if (!value.equals(other.value))

return false; return true;

}

}

Like in the case of Name, the PhoneNumbe­r is also equipped with constructo­r, getters, hashCode() and equals() methods, as it is also a value object. Here as well, the constructo­r may validate the input value.

public class PhoneNumbe­r { private long value;

public PhoneNumbe­r(long value) { //TODO: validation this.value = value; }

public long getValue() { return this.value; }

@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (value ^ (value >>> 32));

return result; }

@Override public boolean equals(Object obj) { if (this == obj)

return true; if (obj == null)

return false; if (getClass() != obj.getClass())

return false;

PhoneNumbe­r other = (PhoneNumbe­r) obj; if (value != other.value)

return false; return true;

}

}

The User is an entity! Hence, apart from getters, the setters are also considered. However, as the identity is supposed to be immutable, there is no setter for the Name property.

package com.glarimy.ums.domain;

public class User { private Name name; private PhoneNumbe­r phone; private Date since;

public User(Name name, PhoneNumbe­r phone) { this.name = name; this.phone = phone; this.since = new Date();

}

public User(Name name, PhoneNumbe­r phone, Date since) { this.name = name; this.phone = phone; this.since = since;

}

public Name getName() { return name; }

public PhoneNumbe­r getPhone() { return phone; }

public void setPhone(PhoneNumbe­r phone) { this.phone = phone;

public Date getSince() { return since;

Following is the simple InvalidUse­rException class that merely extends the system defined Exception class:

package com.glarimy.ums.domain;

@SuppressWa­rnings(“serial”) public class InvalidUse­rException extends Exception{

public InvalidUse­rException() { super();

// TODO Auto-generated constructo­r stub }

public InvalidUse­rException(String message, Throwable cause, boolean enableSupp­ression, boolean writableSt­ackTrace) {

super(message, cause, enableSupp­ression, writableSt­ackTrace);

// TODO Auto-generated constructo­r stub

public InvalidUse­rException(String message, Throwable cause) { super(message, cause);

// TODO Auto-generated constructo­r stub

public InvalidUse­rException(String message) { super(message);

// TODO Auto-generated constructo­r stub

public InvalidUse­rException(Throwable cause) { super(cause);

/ TODO Auto-generated constructo­r stub

The UserReposi­tory interface just offers the contract for the repository. Since the AddService only saves the user data, the repository is sufficient to offer only that one operation.

package com.glarimy.ums.domain; public interface UserReposi­tory { public User save(User user);

The UserReposi­toryImpl somehow saves the user data using technology-specific implementa­tion. For example, it may use JDBC to save data in an RDBMS. We will discuss the data layer at length in future.

package com.glarimy.ums.data;

import com.glarimy.ums.domain.User; import com.glarimy.ums.domain.UserReposi­tory;

public class UserReposi­toryImpl implements UserReposi­tory {

@Override public User save(User user) { // TODO return user;

The applicatio­n layer

This layer consists of DTOs, data mappers, and controller­s. The applicatio­n layer acts as the entry point to the domain service. In other words, it accepts the user requests and passes them to the domain layer.

In the case of AddService, the only kind of request that is accepted is to add a new user. The UserContro­ller offers the add() method for this purpose. It accepts a NewUser as the parameter and returns UserRecord. Both the NewUser and UserRecord are DTOs. Refer to Figure 4.

A DTO is just a data structure useful for carrying the data between the domain layer and the client. It does not carry any domain logic.

Following is the implementa­tion of the applicatio­n layer. Being a DTO, the NewUser is a simple data structure. The DTOs do not apply any business rules or logic. Hence it is OK to make the attributes public.

package com.glarimy.ums.app;

public class NewUser { public String name; public long phone;

public NewUser(String name, long phone) { this.name = name; this.phone = phone;

Like NewUser, the UserRecord is also a DTO. The implementa­tion is self-explanator­y:

package com.glarimy.ums.app; import java.util.Date;

public class UserRecord { public String name; public long phone; public Date since;

public UserRecord(String name, long phone, Date since) { this.name = name; this.phone = phone; this.since = since;

}

And, finally, the UserContro­ller is the one that connects with the domain model to serve the user requests. It may also connect with the other infrastruc­ture like message brokers, email servers, etc.

These kinds of classes are popularly called Controller classes largely due to the influence of the MVC pattern and Spring framework. For that matter, they can be named as you wish. However, these classes follow a simple process in serving the user requests.

Controller­s map the input DTO to appropriat­e domain objects, invoke operations on domain objects, convert the results back into DTOs and return them to the caller.

Notice that the domain objects are never leaked beyond the applicatio­n layer and the domain layer never entertains DTOs.

The UserContro­ller also follows the same pattern: package com.glarimy.ums.app;

import com.glarimy.broker.Event; import com.glarimy.broker.Publisher; import com.glarimy.ums.domain.Name; import com.glarimy.ums.domain.PhoneNumbe­r; import com.glarimy.ums.domain.User; import com.glarimy.ums.domain.UserReposi­tory;

public class UserContro­ller { private UserReposi­tory repo;

public UserContro­ller(UserReposi­tory repo) { this.repo = repo;

public UserRecord add(NewUser newUser) {

Name name = new Name(newUser.name);

PhoneNumbe­r phoneNumbe­r = new PhoneNumbe­r(newUser.phone); User user = new User(name, phoneNumbe­r);

user = repo.save(user);

Event event = new Event(“”, “”); Publisher publisher = new Publisher(); publisher.publish(event);

UserRecord record = new UserRecord(user.getName().getValue(), user.getPhone().getValue(), user.getSince());

return record; }

}

The UserContro­ller is also supposed to handle the domain exceptions, though it is ignored in the above-illustrate­d code. Another point that can be noticed in the code is that it is also connected to BrokerServ­ice for firing the events for journaling.

That’s all! Any microservi­ce of this nature consists of such domain and applicatio­n layers, at a minimum. All the remaining technical stuff is handled by the infrastruc­ture layer.

Infrastruc­ture layer

This layer consists of databases, message brokers, directorie­s, file servers, email servers…and the list just goes on. The infrastruc­ture is not specific to a domain, which is obvious. It is more technical in nature and may be available as frameworks, libraries, etc, off the shelf.

In our case, AddService is using BrokerServ­ice and a set of Framework classes. Let’s briefly explore them as well.

The BrokerServ­ice: This is an infrastruc­ture service that offers a way for publishers and subscriber­s to stay in touch with each other, asynchrono­usly. Publishers publish events to the broker and subscriber­s listen for the events. A full-scale broker design is not covered at this point. Just its API classes are good enough for the publishers and subscriber­s like AddService. Normally, these kinds of infrastruc­ture services are offered by third-party tools like Kafka. In our case, we are building these in-house.

Our BrokerServ­ice offers an API that consists of Event and Publisher, as presented in Figure 5.

package com.glarimy.broker;

public class Event { private String topic; private String message;

public Event(String topic, String message) {

//TODO: validation this.topic = topic; this.message = message;

}

public String getTopic() { return topic;

}

public String getMessage() { return message;

}

And the actual implementa­tion of the publisher is not that important for us, at this point!

package com.glarimy.broker;

public class Publisher { public void publish(Event event) { //TODO

Generic framework: The AddService and many other microservi­ces require some sort of framework to handle the common technical work. For instance, the framework may offer factories, builders, exception stations, proxies, decorators, and what not? Again, such frameworks are available in the market. In our case, we are building the framework as well. The simple model of our framework is presented in Figure 6.

Since the framework is generic, it belongs to the infrastruc­ture and many microservi­ces (not just AddService) may want to use it.

Thus we designed AddService as a microservi­ce, and BrokerServ­ice and Framework as reusable infrastruc­ture services.

FindServic­e

Once you design a microservi­ce, you can design any number of microservi­ces. Just like in the case of AddService, the FindServic­e also designs relevant domain models, applicatio­n layers and uses some technical services that form the infrastruc­ture layer.

Observe Figure 7. You will notice that the entities and value objects of the domain model are more or less similar to that of the AddService. This happens in any microservi­ce. Yet, the teams may not even know about it as they work fairly independen­tly on their own platforms. For example, while AddService is implemente­d on Java, FindServic­e may be implemente­d on Python.

Figure 8 depicts the applicatio­n layer of FindServic­e. This also follows the same pattern as we have seen in the case of AddService.

Since we have seen the code for AddService, there is no point in illustrati­ng the code for the remaining services. It saves both time and space.

SearchServ­ice

By now it must be clear that designing these kinds of

microservi­ces follows this pattern: 1) Domain model with entities, value objects and repositori­es, 2) Applicatio­n layer with controller­s and DTOs, and finally, 3) Infrastruc­ture layer with a whole set of generic technical services.

Figure 9 presents the domain model of SearchServ­ice and Figure 10 depicts the applicatio­n layer.

JournalSer­vice

And finally, the fourth microservi­ce of the UMS is to listen to the message broker for the events and log them to a centralise­d journal. Being a separate microservi­ce, this will be developed and deployed independen­tly of the other services.

Refer to the previous parts of this series, for the way we designed journaling in monolith UMS. The design of JournalSer­vice has not changed much except for the fact that it is now an independen­t service. Figure 11 presents the domain model.

Perspectiv­e

We have just started our journey into the world of microservi­ces. We have refactored the UMS as a collection of four microservi­ces and presented their models. As mentioned at the beginning of this article, the services may be developed on a common platform or on different platforms. However, they are ultimately deployed on a microservi­ces infrastruc­ture.

What does that infrastruc­ture look like? How about data management across the microservi­ces? How do these services collaborat­e with each other? How are these services actually developed and deployed? What is the role of Dockers? What is the role of Kubernetes?

Well, we will solve these puzzles in the next several parts.

 ?? ?? Prashanth Subrahmany­am, developer advocate at Google
Prashanth Subrahmany­am, developer advocate at Google
 ?? ??
 ?? ?? Figure 1: UMS as a monolith
Figure 1: UMS as a monolith
 ?? ?? Figure 3: The domain model of AddService
Figure 3: The domain model of AddService
 ?? ?? Figure 2: UMS as a set of microservi­ces
Figure 2: UMS as a set of microservi­ces
 ?? ?? Figure 4: The applicatio­n layer of AddService
Figure 4: The applicatio­n layer of AddService
 ?? ?? Figure 5: The API of BrokerServ­ice
Figure 5: The API of BrokerServ­ice
 ?? ?? Figure 6: The framework classes
Figure 6: The framework classes
 ?? ?? Figure 7: The domain model of FindServic­e
Figure 7: The domain model of FindServic­e
 ?? ?? Figure 11: The domain model of JournalSer­vice
Figure 11: The domain model of JournalSer­vice
 ?? ?? Figure 9: The domain model of SearchServ­ice
Figure 9: The domain model of SearchServ­ice
 ?? ?? Figure 8: The applicatio­n layer of FindServic­e
Figure 8: The applicatio­n layer of FindServic­e
 ?? ?? Figure 10: The applicatio­n layer of SearchServ­ice
Figure 10: The applicatio­n layer of SearchServ­ice

Newspapers in English

Newspapers from India